From c19f1f7e21c93d1f13b4f44a8a615f65af577559 Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期日, 16 八月 2026 15:10:24 +0800
Subject: [PATCH] 添加 android 端接口
---
src/config/config.local.ts | 4
src/modules/push/service/sorterapp.ts | 124 ++
src/modules/base/db.json | 14
src/modules/operation/entity/wastesale.ts | 4
src/modules/shop/controller/app/sorter.ts | 86 +
src/modules/basicdata/controller/app/options.ts | 40
src/modules/push/controller/app/sorter.ts | 43
src/modules/report/controller/admin/dailysorter.ts | 33
src/modules/shop/service/sorter.ts | 531 +++++++++
src/modules/operation/service/wastesale.ts | 93 +
src/modules/push/entity/sorterhealth.ts | 18
src/modules/shop/entity/recycleappointmentlog.ts | 1
src/modules/report/service/taskservicesorter.ts | 922 +++++++++++++++
src/modules/operation/controller/app/wastesale.ts | 31
src/modules/shop/entity/recycleorder.ts | 2
src/modules/push/controller/app/sorterattendance.ts | 31
src/modules/report/service/taskservice.ts | 2
src/modules/operation/controller/admin/wastesale.ts | 8
src/modules/report/service/screen.ts | 869 ++++++++++++++
src/modules/basicdata/service/options.ts | 95 +
src/modules/report/controller/admin/screen.ts | 17
src/modules/report/service/dailysorter.ts | 14
src/modules/push/service/sorterattendance.ts | 104 +
src/modules/report/entity/dailysorter.ts | 191 +++
src/modules/report/service/ExecutorSorterDaily.ts | 84 +
src/modules/user/service/login.ts | 16
src/modules/order/controller/app/sorter.ts | 34
src/modules/shop/entity/recycleappointment.ts | 2
src/modules/order/service/info.ts | 71 +
29 files changed, 3,459 insertions(+), 25 deletions(-)
diff --git a/src/config/config.local.ts b/src/config/config.local.ts
index 80bbc57..4cb48c9 100644
--- a/src/config/config.local.ts
+++ b/src/config/config.local.ts
@@ -6,6 +6,10 @@
* 鏈湴寮�鍙� npm run dev 璇诲彇鐨勯厤缃枃浠�
*/
export default {
+ koa: {
+ port: 8001,
+ hostname: '0.0.0.0',
+ },
typeorm: {
dataSource: {
default: {
diff --git a/src/modules/base/db.json b/src/modules/base/db.json
index 558a743..82262f5 100644
--- a/src/modules/base/db.json
+++ b/src/modules/base/db.json
@@ -22,6 +22,20 @@
"remark": "鍟嗗煄纰崇Н鍒嗘敮浠橈細鎵�闇�绉垎 = 璁㈠崟閲戦 * carbonratio锛岄粯璁� 100"
},
{
+ "keyName": "hitup.score",
+ "name": "鎵撳崱纰崇Н鍒�",
+ "data": "1",
+ "dataType": 0,
+ "remark": "鍒嗘嫞鍛樼粰瀛︾敓/鑰佸笀鎵撳崱鏃讹紝姣忔澧炲姞鐨勭⒊绉垎"
+ },
+ {
+ "keyName": "hitup.maxmium.perday",
+ "name": "鎵撳崱姣忔棩涓婇檺",
+ "data": "1",
+ "dataType": 0,
+ "remark": "鍚屼竴鐢ㄦ埛鍚屼竴澶╂渶澶氭墦鍗℃鏁帮紝瓒呰繃鍒欐彁绀哄紓甯�"
+ },
+ {
"keyName": "userAgreement",
"name": "鐢ㄦ埛鍗忚",
"data": "<h3 style=\"text-align: center;\"><strong>鐢ㄦ埛鍗忚</strong></h3><p><br></p><p>xxxxxx</p>",
diff --git a/src/modules/basicdata/controller/app/options.ts b/src/modules/basicdata/controller/app/options.ts
new file mode 100644
index 0000000..16751dd
--- /dev/null
+++ b/src/modules/basicdata/controller/app/options.ts
@@ -0,0 +1,40 @@
+import {
+ CoolController,
+ BaseController,
+ CoolUrlTag,
+ TagTypes,
+ CoolTag,
+} from '@cool-midway/core';
+import { Body, Inject, Post } from '@midwayjs/core';
+import { BasicdataOptionsService } from '../../service/options';
+
+/**
+ * 鍒嗘嫞鍛樺熀纭�鏁版嵁涓嬫媺
+ */
+@CoolUrlTag()
+@CoolController()
+export class AppBasicdataOptionsController extends BaseController {
+ @Inject()
+ basicdataOptionsService: BasicdataOptionsService;
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/sites', { summary: '绔欑偣鍒楄〃' })
+ async sites(
+ @Body('sorterId') sorterId: string,
+ @Body('siteType') siteType: string
+ ) {
+ return this.ok(await this.basicdataOptionsService.sites(sorterId, siteType));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/wastetypes', { summary: '鍙洖鏀跺瀮鍦剧绫�' })
+ async wastetypes(@Body('sorterId') sorterId: string) {
+ return this.ok(await this.basicdataOptionsService.wastetypes(sorterId));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/buyers', { summary: '鏀惰喘鏂瑰垪琛�' })
+ async buyers(@Body('sorterId') sorterId: string) {
+ return this.ok(await this.basicdataOptionsService.buyers(sorterId));
+ }
+}
diff --git a/src/modules/basicdata/service/options.ts b/src/modules/basicdata/service/options.ts
new file mode 100644
index 0000000..c63bb06
--- /dev/null
+++ b/src/modules/basicdata/service/options.ts
@@ -0,0 +1,95 @@
+import { Provide } from '@midwayjs/core';
+import { BaseService, CoolCommException } from '@cool-midway/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { BasicdataSorterEntity } from '../entity/sorter';
+import { BasicdataSiteEntity } from '../entity/site';
+import { BasicdataWastetypeEntity } from '../entity/wastetype';
+import { BasicdataBuyerEntity } from '../entity/buyer';
+
+/**
+ * 鍒嗘嫞鍛樹笅鎷夐�夐」
+ */
+@Provide()
+export class BasicdataOptionsService extends BaseService {
+ @InjectEntityModel(BasicdataSorterEntity)
+ basicdataSorterEntity: Repository<BasicdataSorterEntity>;
+
+ @InjectEntityModel(BasicdataSiteEntity)
+ basicdataSiteEntity: Repository<BasicdataSiteEntity>;
+
+ @InjectEntityModel(BasicdataWastetypeEntity)
+ basicdataWastetypeEntity: Repository<BasicdataWastetypeEntity>;
+
+ @InjectEntityModel(BasicdataBuyerEntity)
+ basicdataBuyerEntity: Repository<BasicdataBuyerEntity>;
+
+ async getSorter(sorterId: string) {
+ const sorter = await this.basicdataSorterEntity.findOneBy({
+ businessId: String(sorterId || '').trim(),
+ });
+ if (!sorter || sorter.status !== 0) {
+ throw new CoolCommException('鍒嗘嫞鍛樿处鍙蜂笉鍙敤');
+ }
+ return sorter;
+ }
+
+ async sites(sorterId: string, siteType?: string) {
+ const sorter = await this.getSorter(sorterId);
+ const find = this.basicdataSiteEntity
+ .createQueryBuilder('a')
+ .where('a.status = :st', { st: 1 });
+ if (siteType) {
+ find.andWhere('a.siteType = :siteType', { siteType });
+ }
+ if (sorter.departmentId) {
+ find.andWhere('CAST(a.departmentId AS CHAR) = :deptId', {
+ deptId: String(sorter.departmentId),
+ });
+ }
+ find.orderBy('a.id', 'ASC');
+ const list = await find.getMany();
+ // 鏀堕泦鐐逛负鍙�夐」锛氭湰閮ㄩ棬娌℃湁鏃惰繑鍥炲叏閮ㄦ敹闆嗙偣锛岄伩鍏嶄笅鎷夌┖鐧�
+ if (!list.length && siteType === '鏀堕泦鐐�') {
+ return this.basicdataSiteEntity.find({
+ where: { status: 1, siteType: '鏀堕泦鐐�' },
+ order: { id: 'ASC' },
+ });
+ }
+ return list;
+ }
+
+ async wastetypes(sorterId: string) {
+ await this.getSorter(sorterId);
+ const list = await this.basicdataWastetypeEntity.find({
+ where: { status: 1, isRecyclable: 1 },
+ order: { id: 'ASC' },
+ });
+ if (list.length) {
+ return list;
+ }
+ return this.basicdataWastetypeEntity.find({
+ where: { status: 1 },
+ order: { id: 'ASC' },
+ });
+ }
+
+ async buyers(sorterId: string) {
+ const sorter = await this.getSorter(sorterId);
+ const find = this.basicdataBuyerEntity.createQueryBuilder('a');
+ if (sorter.departmentId) {
+ find.where(
+ '(CAST(a.departmentId AS CHAR) = :deptId OR a.departmentId IS NULL)',
+ { deptId: String(sorter.departmentId) }
+ );
+ }
+ find.orderBy('a.id', 'ASC');
+ const list = await find.getMany();
+ if (list.length) {
+ return list;
+ }
+ return this.basicdataBuyerEntity.find({
+ order: { id: 'ASC' },
+ });
+ }
+}
diff --git a/src/modules/operation/controller/admin/wastesale.ts b/src/modules/operation/controller/admin/wastesale.ts
index 6a24db6..d2b3f43 100644
--- a/src/modules/operation/controller/admin/wastesale.ts
+++ b/src/modules/operation/controller/admin/wastesale.ts
@@ -2,7 +2,7 @@
import { CoolController, BaseController } from '@cool-midway/core';
import { OperationWastesaleEntity } from '../../entity/wastesale';
import { OperationWastesaleService } from '../../service/wastesale';
-import { BaseSysUserEntity } from '../../../base/entity/sys/user';
+import { BasicdataSorterEntity } from '../../../basicdata/entity/sorter';
import { BasicdataBuyerEntity } from '../../../basicdata/entity/buyer'
import { BaseSysDepartmentEntity } from '../../../base/entity/sys/department';
import { BasicdataSiteEntity } from '../../../basicdata/entity/site'
@@ -27,7 +27,7 @@
'b.name AS departmentName',
'c.businessName AS siteName',
'd.name AS wasteName',
- 'e.name AS handlerName',
+ 'e.businessName AS handlerName',
'f.name AS buyerName',
],
join: [
@@ -50,9 +50,9 @@
type: 'leftJoin',
},
{
- entity: BaseSysUserEntity,
+ entity: BasicdataSorterEntity,
alias: 'e',
- condition: 'a.handlerId = e.userId',
+ condition: 'a.handlerId = e.businessId',
type: 'leftJoin',
},
{
diff --git a/src/modules/operation/controller/app/wastesale.ts b/src/modules/operation/controller/app/wastesale.ts
new file mode 100644
index 0000000..508afe5
--- /dev/null
+++ b/src/modules/operation/controller/app/wastesale.ts
@@ -0,0 +1,31 @@
+import {
+ CoolController,
+ BaseController,
+ CoolUrlTag,
+ TagTypes,
+ CoolTag,
+} from '@cool-midway/core';
+import { Body, Inject, Post } from '@midwayjs/core';
+import { OperationWastesaleService } from '../../service/wastesale';
+
+/**
+ * 鍒嗘嫞鍛�-鍙洖鏀剁墿閿�鍞�
+ */
+@CoolUrlTag()
+@CoolController()
+export class AppOperationWastesaleController extends BaseController {
+ @Inject()
+ operationWastesaleService: OperationWastesaleService;
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/saleAdd', { summary: '鏂板閿�鍞褰�' })
+ async saleAdd(@Body() body) {
+ return this.ok(await this.operationWastesaleService.appAdd(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/salePage', { summary: '鎴戠殑閿�鍞褰�' })
+ async salePage(@Body() body) {
+ return this.ok(await this.operationWastesaleService.appPage(body));
+ }
+}
diff --git a/src/modules/operation/entity/wastesale.ts b/src/modules/operation/entity/wastesale.ts
index 67423b7..ca1c7bd 100644
--- a/src/modules/operation/entity/wastesale.ts
+++ b/src/modules/operation/entity/wastesale.ts
@@ -37,8 +37,8 @@
totalPrice: number;
@Index()
- @Column({ comment: '缁忔墜浜篒D', type: 'bigint' })
- handlerId: number;
+ @Column({ comment: '缁忔墜浜篒D', length: 64 })
+ handlerId: string;
handlerName: string;
diff --git a/src/modules/operation/service/wastesale.ts b/src/modules/operation/service/wastesale.ts
index 70b0f4c..8e6cd6f 100644
--- a/src/modules/operation/service/wastesale.ts
+++ b/src/modules/operation/service/wastesale.ts
@@ -1,8 +1,12 @@
import { Provide } from '@midwayjs/core';
-import { BaseService } from '@cool-midway/core';
+import { BaseService, CoolCommException } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { OperationWastesaleEntity } from '../entity/wastesale';
+import { BasicdataSorterEntity } from '../../basicdata/entity/sorter';
+import { BasicdataWastetypeEntity } from '../../basicdata/entity/wastetype';
+import { BasicdataBuyerEntity } from '../../basicdata/entity/buyer';
+import { BasicdataSiteEntity } from '../../basicdata/entity/site';
/**
* 鍙洖鏀剁墿鏀剁泭鏈嶅姟
@@ -12,14 +16,97 @@
@InjectEntityModel(OperationWastesaleEntity)
operationWastesaleEntity: Repository<OperationWastesaleEntity>;
+ @InjectEntityModel(BasicdataSorterEntity)
+ basicdataSorterEntity: Repository<BasicdataSorterEntity>;
+
+ @InjectEntityModel(BasicdataWastetypeEntity)
+ basicdataWastetypeEntity: Repository<BasicdataWastetypeEntity>;
+
+ @InjectEntityModel(BasicdataBuyerEntity)
+ basicdataBuyerEntity: Repository<BasicdataBuyerEntity>;
+
+ @InjectEntityModel(BasicdataSiteEntity)
+ basicdataSiteEntity: Repository<BasicdataSiteEntity>;
+
/**
* 鍐欏叆鍓嶈绠楁�讳环
*/
async modifyBefore(data: any, type: 'add' | 'update') {
if (type === 'add' || type === 'update') {
if (data.weight !== undefined && data.unitPrice !== undefined) {
- data.totalPrice = Number((Number(data.weight) * Number(data.unitPrice)).toFixed(2));
+ data.totalPrice = Number(
+ (Number(data.weight) * Number(data.unitPrice)).toFixed(2)
+ );
}
}
}
-}
\ No newline at end of file
+
+ async getSorter(sorterId: string) {
+ const sorter = await this.basicdataSorterEntity.findOneBy({
+ businessId: String(sorterId || '').trim(),
+ });
+ if (!sorter || sorter.status !== 0) {
+ throw new CoolCommException('鍒嗘嫞鍛樿处鍙蜂笉鍙敤');
+ }
+ return sorter;
+ }
+
+ /**
+ * 鍒嗘嫞鍛樻柊澧為攢鍞褰�
+ */
+ async appAdd(param: any) {
+ const sorter = await this.getSorter(param.sorterId);
+ const weight = Number(param.weight);
+ let unitPrice = Number(param.unitPrice);
+ if (!weight || weight <= 0) {
+ throw new CoolCommException('璇峰~鍐欓噸閲�');
+ }
+ if (!param.wasteCode) {
+ throw new CoolCommException('璇烽�夋嫨鍨冨溇绉嶇被');
+ }
+ if (!param.buyerCode) {
+ throw new CoolCommException('璇烽�夋嫨鏀惰喘鏂�');
+ }
+ if (!unitPrice) {
+ const waste = await this.basicdataWastetypeEntity.findOneBy({
+ code: param.wasteCode,
+ });
+ unitPrice = Number(waste?.price || 0);
+ }
+ const data: any = {
+ departmentId: param.departmentId || sorter.departmentId,
+ siteId: param.siteId,
+ wasteCode: param.wasteCode,
+ dateTime: param.dateTime || new Date(),
+ weight,
+ unitPrice,
+ totalPrice: Number((weight * unitPrice).toFixed(2)),
+ handlerId: sorter.businessId,
+ buyerCode: param.buyerCode,
+ };
+ await this.operationWastesaleEntity.save(data);
+ return data;
+ }
+
+ /**
+ * 鏌ョ湅褰撳墠鍒嗘嫞鍛樼殑閿�鍞褰�
+ */
+ async appPage(query: any) {
+ const { sorterId, page = 1, size = 10 } = query || {};
+ const sorter = await this.getSorter(sorterId);
+ const find = this.operationWastesaleEntity
+ .createQueryBuilder('a')
+ .select([
+ 'a.*',
+ 'c.businessName AS siteName',
+ 'd.name AS wasteName',
+ 'f.name AS buyerName',
+ ])
+ .leftJoin(BasicdataSiteEntity, 'c', 'a.siteId = c.businessId')
+ .leftJoin(BasicdataWastetypeEntity, 'd', 'a.wasteCode = d.code')
+ .leftJoin(BasicdataBuyerEntity, 'f', 'a.buyerCode = f.code')
+ .where('a.handlerId = :hid', { hid: sorter.businessId })
+ .orderBy('a.dateTime', 'DESC');
+ return this.entityRenderPage(find, { ...query, page, size }, false);
+ }
+}
diff --git a/src/modules/order/controller/app/sorter.ts b/src/modules/order/controller/app/sorter.ts
new file mode 100644
index 0000000..67eb4c6
--- /dev/null
+++ b/src/modules/order/controller/app/sorter.ts
@@ -0,0 +1,34 @@
+import {
+ CoolController,
+ BaseController,
+ CoolUrlTag,
+ TagTypes,
+ CoolTag,
+} from '@cool-midway/core';
+import { Body, Inject, Post } from '@midwayjs/core';
+import { OrderInfoService } from '../../service/info';
+
+/**
+ * 鍒嗘嫞鍛�-鏍″洯鍟嗗煄璁㈠崟
+ */
+@CoolUrlTag()
+@CoolController()
+export class AppOrderSorterController extends BaseController {
+ @Inject()
+ orderInfoService: OrderInfoService;
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/orderPage', { summary: '鏍″洯鍟嗗煄璁㈠崟鍒嗛〉' })
+ async orderPage(@Body() body) {
+ return this.ok(await this.orderInfoService.sorterPage(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/deliver', { summary: '鐜板満鍙戞斁' })
+ async deliver(
+ @Body('orderId') orderId: number,
+ @Body('sorterId') sorterId: string
+ ) {
+ return this.ok(await this.orderInfoService.sorterDeliver(orderId, sorterId));
+ }
+}
diff --git a/src/modules/order/service/info.ts b/src/modules/order/service/info.ts
index 48786ee..384477c 100644
--- a/src/modules/order/service/info.ts
+++ b/src/modules/order/service/info.ts
@@ -7,6 +7,7 @@
import { OrderGoodsEntity } from '../entity/goods';
import * as moment from 'moment';
import { UserAddressService } from '../../user/service/address';
+import { UserInfoEntity } from '../../user/entity/info';
import { Action, OrderQueue } from '../queue/order';
import { OrderPayService } from './pay';
import { PluginService } from '../../plugin/service/info';
@@ -14,6 +15,7 @@
import { MarketCouponInfoService } from '../../market/service/coupon/info';
import { MarketCouponUserEntity } from '../../market/entity/coupon/user';
import { BaseSysParamService } from '../../base/service/sys/param';
+import { BasicdataSorterEntity } from '../../basicdata/entity/sorter';
import BigNumber from 'bignumber.js';
/**
@@ -29,6 +31,9 @@
@InjectEntityModel(MarketCouponUserEntity)
marketCouponUserEntity: Repository<MarketCouponUserEntity>;
+
+ @InjectEntityModel(BasicdataSorterEntity)
+ basicdataSorterEntity: Repository<BasicdataSorterEntity>;
@Inject()
orderGoodsService: OrderGoodsService;
@@ -418,6 +423,72 @@
}
/**
+ * 鍒嗘嫞鍛樻煡鐪嬫牎鍥晢鍩庤鍗曪紙鎸夊鏍¤繃婊わ級
+ */
+ async sorterPage(query: any) {
+ const { sorterId, status, page = 1, size = 10, keyWord } = query || {};
+ if (!sorterId) {
+ throw new CoolCommException('缂哄皯鍒嗘嫞鍛樹俊鎭�');
+ }
+ const sorter = await this.basicdataSorterEntity.findOneBy({
+ businessId: String(sorterId),
+ });
+ if (!sorter || sorter.status !== 0) {
+ throw new CoolCommException('鍒嗘嫞鍛樿处鍙蜂笉鍙敤');
+ }
+ const find = this.orderInfoEntity
+ .createQueryBuilder('a')
+ .select([
+ 'a.*',
+ 'b.nickName AS nickName',
+ 'b.phone AS phone',
+ 'b.unionid AS unionid',
+ ])
+ .leftJoin(UserInfoEntity, 'b', 'a.userId = b.unionid');
+ if (sorter.departmentId) {
+ find.andWhere('b.departmentId = :deptId', {
+ deptId: sorter.departmentId,
+ });
+ }
+ if (status !== undefined && status !== null && status !== '') {
+ find.andWhere('a.status = :status', { status: Number(status) });
+ }
+ if (keyWord) {
+ find.andWhere(
+ '(a.orderNum LIKE :kw OR a.title LIKE :kw OR b.nickName LIKE :kw OR b.phone LIKE :kw)',
+ { kw: `%${keyWord}%` }
+ );
+ }
+ find.orderBy('a.createTime', 'DESC');
+ const result = await this.entityRenderPage(
+ find,
+ { ...query, page, size },
+ false
+ );
+ const ids = (result.list || []).map((e: any) => e.id);
+ if (ids.length) {
+ const goodsList = await this.orderGoodsService.getByOrderIds(ids);
+ for (const item of result.list) {
+ item.goodsList = goodsList.filter(e => e.orderId == item.id);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * 鏍″洯鐜板満鍙戞斁锛堜笉璧扮墿娴侊級
+ */
+ async sorterDeliver(orderId: number, sorterId: string) {
+ if (!sorterId) {
+ throw new CoolCommException('缂哄皯鍒嗘嫞鍛樹俊鎭�');
+ }
+ return this.deliver(orderId, {
+ company: '鐜板満鍙戞斁',
+ num: String(sorterId),
+ });
+ }
+
+ /**
* 妫�鏌ュ簱瀛�
* @param goodsList
*/
diff --git a/src/modules/push/controller/app/sorter.ts b/src/modules/push/controller/app/sorter.ts
new file mode 100644
index 0000000..10d708b
--- /dev/null
+++ b/src/modules/push/controller/app/sorter.ts
@@ -0,0 +1,43 @@
+import {
+ CoolController,
+ BaseController,
+ CoolUrlTag,
+ TagTypes,
+ CoolTag,
+} from '@cool-midway/core';
+import { Body, Inject, Post } from '@midwayjs/core';
+import { PushSorterAppService } from '../../service/sorterapp';
+
+/**
+ * 鍒嗘嫞鍛樹笂鎶�
+ */
+@CoolUrlTag()
+@CoolController()
+export class AppPushSorterController extends BaseController {
+ @Inject()
+ pushSorterAppService: PushSorterAppService;
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/health', { summary: '鍋ュ悍鐩戞祴涓婃姤' })
+ async health(@Body() body) {
+ return this.ok(await this.pushSorterAppService.reportHealth(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/gps', { summary: 'GPS浣嶇疆涓婃姤' })
+ async gps(@Body() body) {
+ return this.ok(await this.pushSorterAppService.reportGps(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/siteweightAdd', { summary: '鍘嬬缉绔欎汉宸ョО閲嶄笂鎶�' })
+ async siteweightAdd(@Body() body) {
+ return this.ok(await this.pushSorterAppService.addSiteweight(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/siteweightPage', { summary: '鍘嬬缉绔欎汉宸ョО閲嶈褰�' })
+ async siteweightPage(@Body() body) {
+ return this.ok(await this.pushSorterAppService.siteweightPage(body));
+ }
+}
diff --git a/src/modules/push/controller/app/sorterattendance.ts b/src/modules/push/controller/app/sorterattendance.ts
new file mode 100644
index 0000000..ec98faf
--- /dev/null
+++ b/src/modules/push/controller/app/sorterattendance.ts
@@ -0,0 +1,31 @@
+import {
+ CoolController,
+ BaseController,
+ CoolUrlTag,
+ TagTypes,
+ CoolTag,
+} from '@cool-midway/core';
+import { Body, Inject, Post } from '@midwayjs/core';
+import { PushSorterAttendanceService } from '../../service/sorterattendance';
+
+/**
+ * 鍒嗘嫞鍛樺伐浣滅鍒�
+ */
+@CoolUrlTag()
+@CoolController()
+export class AppPushSorterattendanceController extends BaseController {
+ @Inject()
+ pushSorterAttendanceService: PushSorterAttendanceService;
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/checkIn', { summary: '涓婄彮绛惧埌' })
+ async checkIn(@Body() body) {
+ return this.ok(await this.pushSorterAttendanceService.checkIn(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/checkOut', { summary: '涓嬬彮绛鹃��' })
+ async checkOut(@Body() body) {
+ return this.ok(await this.pushSorterAttendanceService.checkOut(body));
+ }
+}
diff --git a/src/modules/push/entity/sorterhealth.ts b/src/modules/push/entity/sorterhealth.ts
index c09edd6..3b2962e 100644
--- a/src/modules/push/entity/sorterhealth.ts
+++ b/src/modules/push/entity/sorterhealth.ts
@@ -19,22 +19,26 @@
@Column({ comment: '鏁版嵁鏃堕棿' })
uploadTime: Date;
- @Column({ comment: '浣撴俯', type: 'decimal', precision: 3, scale: 1 })
+ @Column({ comment: '浣撴俯', type: 'decimal', precision: 3, scale: 1, nullable: true })
temperature: number;
- @Column({ comment: '蹇冪巼' })
+ @Column({ comment: '蹇冪巼', nullable: true })
heartRate: number;
- @Column({ comment: '鏀剁缉鍘�' })
+ @Column({ comment: '鏀剁缉鍘�', nullable: true })
bloodPressureHigh: number;
- @Column({ comment: '鑸掑紶鍘�' })
+ @Column({ comment: '鑸掑紶鍘�', nullable: true })
bloodPressureLow: number;
- @Column({ comment: '琛�姘чケ鍜屽害', type: 'decimal', precision: 4, scale: 1 })
+ @Column({ comment: '琛�姘чケ鍜屽害', type: 'decimal', precision: 4, scale: 1, nullable: true })
bloodOxygen: number;
-
- // add more if needed
+
+ @Column({ comment: '姝ユ暟', nullable: true, default: 0 })
+ stepCount: number;
+
+ @Column({ comment: '鏁版嵁绫诲瀷', dict: ['鍛ㄦ湡', '骞冲潎', '寮傚父'], default: '鍛ㄦ湡', nullable: true })
+ dataType: string;
}
\ No newline at end of file
diff --git a/src/modules/push/service/sorterapp.ts b/src/modules/push/service/sorterapp.ts
new file mode 100644
index 0000000..5a579fc
--- /dev/null
+++ b/src/modules/push/service/sorterapp.ts
@@ -0,0 +1,124 @@
+import { Provide } from '@midwayjs/core';
+import { BaseService, CoolCommException } from '@cool-midway/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { BasicdataSorterEntity } from '../../basicdata/entity/sorter';
+import { PushSorterHealthEntity } from '../entity/sorterhealth';
+import { PushSorterGpsEntity } from '../entity/sortergps';
+import { PushSiteweightEntity } from '../entity/siteweight';
+import { BasicdataSiteEntity } from '../../basicdata/entity/site';
+
+/**
+ * 鍒嗘嫞鍛樹笂鎶ワ紙鍋ュ悍銆丟PS銆佸帇缂╃珯绉伴噸锛�
+ */
+@Provide()
+export class PushSorterAppService extends BaseService {
+ @InjectEntityModel(BasicdataSorterEntity)
+ basicdataSorterEntity: Repository<BasicdataSorterEntity>;
+
+ @InjectEntityModel(PushSorterHealthEntity)
+ pushSorterHealthEntity: Repository<PushSorterHealthEntity>;
+
+ @InjectEntityModel(PushSorterGpsEntity)
+ pushSorterGpsEntity: Repository<PushSorterGpsEntity>;
+
+ @InjectEntityModel(PushSiteweightEntity)
+ pushSiteweightEntity: Repository<PushSiteweightEntity>;
+
+ @InjectEntityModel(BasicdataSiteEntity)
+ basicdataSiteEntity: Repository<BasicdataSiteEntity>;
+
+ async getSorter(sorterId: string) {
+ const sorter = await this.basicdataSorterEntity.findOneBy({
+ businessId: String(sorterId || '').trim(),
+ });
+ if (!sorter || sorter.status !== 0) {
+ throw new CoolCommException('鍒嗘嫞鍛樿处鍙蜂笉鍙敤');
+ }
+ return sorter;
+ }
+
+ async reportHealth(param: any) {
+ const sorter = await this.getSorter(param.sorterId);
+ const row = await this.pushSorterHealthEntity.save({
+ idCard: sorter.businessId,
+ departmentId: sorter.departmentId,
+ uploadTime: param.uploadTime ? new Date(param.uploadTime) : new Date(),
+ temperature: param.temperature,
+ heartRate: param.heartRate,
+ bloodPressureHigh: param.bloodPressureHigh,
+ bloodPressureLow: param.bloodPressureLow,
+ bloodOxygen: param.bloodOxygen,
+ stepCount: param.stepCount || 0,
+ dataType: param.dataType || '鍛ㄦ湡',
+ });
+ return row;
+ }
+
+ async reportGps(param: any) {
+ const sorter = await this.getSorter(param.sorterId);
+ if (param.longitude == null || param.latitude == null) {
+ throw new CoolCommException('缂哄皯瀹氫綅淇℃伅');
+ }
+ return this.pushSorterGpsEntity.save({
+ idCard: sorter.businessId,
+ name: sorter.businessName,
+ departmentId: sorter.departmentId,
+ uploadTime: param.uploadTime ? new Date(param.uploadTime) : new Date(),
+ longitude: Number(param.longitude),
+ latitude: Number(param.latitude),
+ height: Number(param.height || 0),
+ speed: Number(param.speed || 0),
+ accuracy: Number(param.accuracy || 0),
+ });
+ }
+
+ async addSiteweight(param: any) {
+ const sorter = await this.getSorter(param.sorterId);
+ if (!param.businessId) {
+ throw new CoolCommException('璇烽�夋嫨鍘嬬缉绔�');
+ }
+ const weight = Number(param.weight);
+ if (!weight || weight <= 0) {
+ throw new CoolCommException('璇峰~鍐欏瀮鍦鹃噸閲�');
+ }
+ const site = await this.basicdataSiteEntity.findOneBy({
+ businessId: String(param.businessId),
+ });
+ return this.pushSiteweightEntity.save({
+ businessId: param.businessId,
+ departmentId: param.departmentId || site?.departmentId || sorter.departmentId,
+ deviceNo: param.deviceNo || '浜哄伐濉姤',
+ uploadTime: param.uploadTime ? new Date(param.uploadTime) : new Date(),
+ garbageType: param.garbageType || 'SW64',
+ weight,
+ remark: param.remark,
+ extraData: JSON.stringify({
+ source: 'sorter-app',
+ sorterId: sorter.businessId,
+ }),
+ });
+ }
+
+ async siteweightPage(query: any) {
+ const { sorterId, page = 1, size = 10 } = query || {};
+ const sorter = await this.getSorter(sorterId);
+ const find = this.pushSiteweightEntity
+ .createQueryBuilder('a')
+ .select([
+ 'a.*',
+ 'd.businessName AS businessName',
+ 'd.siteType AS siteType',
+ ])
+ .leftJoin(BasicdataSiteEntity, 'd', 'a.businessId = d.businessId')
+ .where('d.siteType = :st', { st: '鍘嬬缉绔�' })
+ .andWhere('a.deviceNo = :dn', { dn: '浜哄伐濉姤' });
+ if (sorter.departmentId) {
+ find.andWhere('a.departmentId = :deptId', {
+ deptId: sorter.departmentId,
+ });
+ }
+ find.orderBy('a.uploadTime', 'DESC');
+ return this.entityRenderPage(find, { ...query, page, size }, false);
+ }
+}
diff --git a/src/modules/push/service/sorterattendance.ts b/src/modules/push/service/sorterattendance.ts
index 1546c35..0852926 100644
--- a/src/modules/push/service/sorterattendance.ts
+++ b/src/modules/push/service/sorterattendance.ts
@@ -1,14 +1,112 @@
import { Provide } from '@midwayjs/core';
-import { BaseService } from '@cool-midway/core';
+import { BaseService, CoolCommException } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
+import * as moment from 'moment';
import { PushSorterAttendanceEntity } from '../entity/sorterattendance';
+import { BasicdataSorterEntity } from '../../basicdata/entity/sorter';
/**
- * 鍒嗘嫞鍛樺仴搴峰憡璀︽湇鍔�
+ * 鍒嗘嫞鍛樼鍒扮閫�
*/
@Provide()
export class PushSorterAttendanceService extends BaseService {
@InjectEntityModel(PushSorterAttendanceEntity)
pushSorterAttendanceEntity: Repository<PushSorterAttendanceEntity>;
-}
\ No newline at end of file
+
+ @InjectEntityModel(BasicdataSorterEntity)
+ basicdataSorterEntity: Repository<BasicdataSorterEntity>;
+
+ async getSorter(sorterId: string) {
+ const sorter = await this.basicdataSorterEntity.findOneBy({
+ businessId: String(sorterId || '').trim(),
+ });
+ if (!sorter || sorter.status !== 0) {
+ throw new CoolCommException('鍒嗘嫞鍛樿处鍙蜂笉鍙敤');
+ }
+ return sorter;
+ }
+
+ private locationOf(param: any) {
+ if (param.longitude == null || param.latitude == null) {
+ throw new CoolCommException('缂哄皯瀹氫綅淇℃伅');
+ }
+ return `${Number(param.longitude)},${Number(param.latitude)}`;
+ }
+
+ private today() {
+ return moment().format('YYYY-MM-DD');
+ }
+
+ private async todayRow(idCard: string) {
+ return this.pushSorterAttendanceEntity
+ .createQueryBuilder('a')
+ .where('a.idCard = :idCard', { idCard })
+ .andWhere('a.workDate = :workDate', { workDate: this.today() })
+ .getOne();
+ }
+
+ async checkIn(param: any) {
+ const sorter = await this.getSorter(param.sorterId);
+ const loc = this.locationOf(param);
+ const now = new Date();
+ const workDate = this.today();
+ let row = await this.todayRow(sorter.businessId);
+ if (row?.checkInTime) {
+ throw new CoolCommException('浠婃棩宸茬鍒�');
+ }
+ if (!row) {
+ row = this.pushSorterAttendanceEntity.create({
+ idCard: sorter.businessId,
+ departmentId: sorter.departmentId,
+ uploadTime: now,
+ workDate: workDate as any,
+ checkInTime: now,
+ checkInLocation: loc,
+ workHours: 0,
+ status: '缂哄崱',
+ });
+ } else {
+ row.departmentId = sorter.departmentId;
+ row.uploadTime = now;
+ row.checkInTime = now;
+ row.checkInLocation = loc;
+ }
+ await this.pushSorterAttendanceEntity.save(row);
+ return {
+ message: '绛惧埌鎴愬姛',
+ workDate,
+ checkInTime: now,
+ checkInLocation: loc,
+ };
+ }
+
+ async checkOut(param: any) {
+ const sorter = await this.getSorter(param.sorterId);
+ const loc = this.locationOf(param);
+ const now = new Date();
+ const workDate = this.today();
+ const row = await this.todayRow(sorter.businessId);
+ if (!row?.checkInTime) {
+ throw new CoolCommException('璇峰厛涓婄彮绛惧埌');
+ }
+ if (row.checkOutTime) {
+ throw new CoolCommException('浠婃棩宸茬閫�');
+ }
+ const start = new Date(row.checkInTime).getTime();
+ const hours = Math.max(0, (now.getTime() - start) / 3600000);
+ row.uploadTime = now;
+ row.checkOutTime = now;
+ row.checkOutLocation = loc;
+ row.workHours = Math.round(hours * 100) / 100;
+ row.status = '姝e父';
+ await this.pushSorterAttendanceEntity.save(row);
+ return {
+ message: '绛鹃��鎴愬姛',
+ workDate,
+ checkOutTime: now,
+ checkOutLocation: loc,
+ workHours: row.workHours,
+ };
+ }
+}
diff --git a/src/modules/report/controller/admin/dailysorter.ts b/src/modules/report/controller/admin/dailysorter.ts
new file mode 100644
index 0000000..7b28a35
--- /dev/null
+++ b/src/modules/report/controller/admin/dailysorter.ts
@@ -0,0 +1,33 @@
+import { CoolController, BaseController } from '@cool-midway/core';
+import { ReportDailySorterEntity } from '../../entity/dailysorter';
+import { ReportDailySorterService } from '../../service/dailysorter';
+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: ReportDailySorterEntity,
+ service: ReportDailySorterService,
+ pageQueryOp: {
+ keyWordLikeFields: ['a.businessName', 'a.businessId'],
+ fieldEq: ['a.date', 'a.businessId', 'a.attendanceStatus'],
+ 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 AdminReportDailySorterController extends BaseController {}
diff --git a/src/modules/report/controller/admin/screen.ts b/src/modules/report/controller/admin/screen.ts
new file mode 100644
index 0000000..c862300
--- /dev/null
+++ b/src/modules/report/controller/admin/screen.ts
@@ -0,0 +1,17 @@
+import { Body, Inject, Post } from '@midwayjs/core';
+import { CoolController, BaseController } from '@cool-midway/core';
+import { ReportScreenService } from '../../service/screen';
+
+/**
+ * 鏍″洯缁胯壊杩愯惀椹鹃┒鑸�
+ */
+@CoolController()
+export class AdminReportScreenController extends BaseController {
+ @Inject()
+ reportScreenService: ReportScreenService;
+
+ @Post('/query', { summary: '椹鹃┒鑸辨眹鎬�' })
+ async query(@Body('range') range: string) {
+ return this.ok(await this.reportScreenService.query(range));
+ }
+}
diff --git a/src/modules/report/entity/dailysorter.ts b/src/modules/report/entity/dailysorter.ts
new file mode 100644
index 0000000..a715be8
--- /dev/null
+++ b/src/modules/report/entity/dailysorter.ts
@@ -0,0 +1,191 @@
+import { BaseEntity } from '../../base/entity/base';
+import { Column, Entity, Index, Unique } from 'typeorm';
+
+/**
+ * 鍒嗘嫞鍛樻瘡鏃ョ粺璁�
+ *
+ * 涓�鍚嶅垎鎷e憳涓�澶╀竴鏉¤褰�
+ */
+@Entity('t_report_daily_sorter')
+@Unique('uk_date_sorter', ['date', 'businessId'])
+export class ReportDailySorterEntity extends BaseEntity {
+ @Index()
+ @Column({
+ comment: '鏃ユ湡',
+ type: 'date',
+ })
+ date: string;
+
+ @Index()
+ @Column({
+ comment: '韬唤璇佸彿',
+ length: 32,
+ })
+ businessId: string;
+
+ @Column({
+ comment: '濮撳悕',
+ length: 64,
+ nullable: true,
+ })
+ businessName: string;
+
+ @Index()
+ @Column({
+ comment: '鎵�灞炲崟浣岻D',
+ type: 'bigint',
+ nullable: true,
+ })
+ departmentId: number;
+
+ @Column({
+ comment: '鎵�灞炲崟浣嶅悕绉�',
+ length: 64,
+ nullable: true,
+ })
+ departmentName: string;
+
+ @Index()
+ @Column({
+ comment: '宸ヤ綔绔欑偣ID',
+ nullable: true,
+ })
+ workSiteId: string;
+
+ @Column({
+ comment: '宸ヤ綔绔欑偣鍚嶇О',
+ length: 64,
+ nullable: true,
+ })
+ workSiteName: string;
+
+ @Column({
+ comment: '绛惧埌鏃堕棿',
+ nullable: true,
+ })
+ checkInTime: Date;
+
+ @Column({
+ comment: '绛鹃��鏃堕棿',
+ nullable: true,
+ })
+ checkOutTime: Date;
+
+ @Column({
+ comment: '宸ヤ綔鏃堕暱(灏忔椂)',
+ type: 'decimal',
+ precision: 5,
+ scale: 2,
+ default: 0.0,
+ })
+ workHours: number;
+
+ @Column({
+ comment: '鑰冨嫟鐘舵��',
+ dict: ['姝e父', '杩熷埌', '鏃╅��', '缂哄崱', '鏃峰伐'],
+ default: '缂哄崱',
+ })
+ attendanceStatus: string;
+
+ @Column({
+ comment: 'GPS鏁版嵁鐐规暟閲�',
+ type: 'int',
+ default: 0,
+ })
+ gpsPointCount: number;
+
+ @Column({
+ comment: '宸ヤ綔杞ㄨ抗閲岀▼(km)',
+ type: 'decimal',
+ precision: 10,
+ scale: 2,
+ default: 0.0,
+ })
+ totalKm: number;
+
+ @Column({
+ comment: '绉诲姩鏃堕暱(鍒嗛挓)',
+ type: 'int',
+ default: 0,
+ })
+ minutesMoving: number;
+
+ @Column({
+ comment: '鍥炴敹璁㈠崟鏁�',
+ type: 'int',
+ default: 0,
+ })
+ orderCount: number;
+
+ @Column({
+ comment: '瀹屾垚璁㈠崟鏁�',
+ type: 'int',
+ default: 0,
+ })
+ completedOrderCount: number;
+
+ @Column({
+ comment: '棰勭害涓婇棬璁㈠崟鏁�',
+ type: 'int',
+ default: 0,
+ })
+ appointmentOrderCount: number;
+
+ @Column({
+ comment: '鍙戞斁纰崇Н鍒�',
+ type: 'decimal',
+ precision: 12,
+ scale: 2,
+ default: 0.0,
+ })
+ totalCarbonPoint: number;
+
+ @Column({
+ comment: '鍋ュ悍鏁版嵁鏁伴噺',
+ type: 'int',
+ default: 0,
+ })
+ healthDataCount: number;
+
+ @Column({
+ comment: '鍋ュ悍寮傚父鏁伴噺',
+ type: 'int',
+ default: 0,
+ })
+ healthAbnormalCount: number;
+
+ @Column({
+ comment: '鍋ュ悍鍛婅鎬绘暟',
+ type: 'int',
+ default: 0,
+ })
+ alarmCount: number;
+
+ @Column({
+ comment: '涓�鑸憡璀︽鏁�',
+ type: 'int',
+ default: 0,
+ })
+ generalAlarmCount: number;
+
+ @Column({
+ comment: '涓瓑鍛婅娆℃暟',
+ type: 'int',
+ default: 0,
+ })
+ mediumAlarmCount: number;
+
+ @Column({
+ comment: '涓ラ噸鍛婅娆℃暟',
+ type: 'int',
+ default: 0,
+ })
+ seriousAlarmCount: number;
+
+ @Column({
+ comment: '鏈鐞嗗憡璀︽鏁�',
+ type: 'int',
+ default: 0,
+ })
+ unhandledAlarmCount: number;
+}
diff --git a/src/modules/report/service/ExecutorSorterDaily.ts b/src/modules/report/service/ExecutorSorterDaily.ts
new file mode 100644
index 0000000..4711e6c
--- /dev/null
+++ b/src/modules/report/service/ExecutorSorterDaily.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 { TaskServiceSorter } from './taskservicesorter'
+
+
+
+@Provide()
+export class ExecutorSorterDaily {
+
+ @InjectEntityModel(ReportJoblogEntity)
+ reportJoblogEntity: Repository<ReportJoblogEntity>;
+
+ @Inject()
+ taskServiceSorter: TaskServiceSorter;
+
+ async execute() {
+ // 鎹炲嚭鎵�鏈夊緟澶勭悊浠诲姟
+ const pendingSiteJobs = await this.reportJoblogEntity.find({
+ where: {
+ status: In(['INIT', 'FAILED']),
+ jobName: In(['SORTER_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.taskServiceSorter.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/dailysorter.ts b/src/modules/report/service/dailysorter.ts
new file mode 100644
index 0000000..6565f7d
--- /dev/null
+++ b/src/modules/report/service/dailysorter.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 { ReportDailySorterEntity } from '../entity/dailysorter';
+
+/**
+ * 鍒嗘嫞鍛樻瘡鏃ユ眹鎬绘湇鍔�
+ */
+@Provide()
+export class ReportDailySorterService extends BaseService {
+ @InjectEntityModel(ReportDailySorterEntity)
+ reportDailySorterEntity: Repository<ReportDailySorterEntity>;
+}
diff --git a/src/modules/report/service/screen.ts b/src/modules/report/service/screen.ts
new file mode 100644
index 0000000..1fbe4b8
--- /dev/null
+++ b/src/modules/report/service/screen.ts
@@ -0,0 +1,869 @@
+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 * as moment from 'moment';
+import { ReportDailysiteEntity } from '../entity/dailysite';
+import { ReportDailyVehicleEntity } from '../entity/dailyvehicle';
+import { ReportDailySorterEntity } from '../entity/dailysorter';
+import { BasicdataWastetypeEntity } from '../../basicdata/entity/wastetype';
+import { BasicdataSorterEntity } from '../../basicdata/entity/sorter';
+import { BasicdataVehicleEntity } from '../../basicdata/entity/vehicle';
+import { BasicdataSiteEntity } from '../../basicdata/entity/site';
+import { RecycleOrderEntity } from '../../shop/entity/recycleorder';
+import { RecycleAppointmentEntity } from '../../shop/entity/recycleappointment';
+import { RecycleTransactionEntity } from '../../shop/entity/transaction';
+import { UserInfoEntity } from '../../user/entity/info';
+import { PushVehicleGpsEntity } from '../../push/entity/vehiclegps';
+import { BaseSysParamEntity } from '../../base/entity/sys/param';
+
+const WASTE_COLORS: Record<string, string> = {
+ SW62: '#16a34a',
+ SW64: '#64748b',
+ SW63: '#f59e0b',
+ SW61: '#ef4444',
+ SW60: '#06b6d4',
+};
+
+const ITEM_COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#a855f7', '#06b6d4', '#ef4444', '#94a3b8'];
+
+type OrdersByType = {
+ axis: string[];
+ '鐜板満鍥炴敹': number[];
+ '棰勭害涓婇棬': number[];
+ '澶у睆鎶曢��': number[];
+ '娲诲姩鍥炴敹': number[];
+};
+
+function n(v: any, d = 0) {
+ const x = Number(v);
+ return Number.isFinite(x) ? x : d;
+}
+
+function round(v: number, d = 2) {
+ const p = 10 ** d;
+ return Math.round(n(v) * p) / p;
+}
+
+@Provide()
+export class ReportScreenService extends BaseService {
+ @Inject()
+ ctx: Context;
+
+ @InjectEntityModel(ReportDailysiteEntity)
+ dailySite: Repository<ReportDailysiteEntity>;
+
+ @InjectEntityModel(ReportDailyVehicleEntity)
+ dailyVehicle: Repository<ReportDailyVehicleEntity>;
+
+ @InjectEntityModel(ReportDailySorterEntity)
+ dailySorter: Repository<ReportDailySorterEntity>;
+
+ @InjectEntityModel(BaseSysParamEntity)
+ paramRepo: Repository<BaseSysParamEntity>;
+
+ @InjectEntityModel(BasicdataWastetypeEntity)
+ wasteTypeRepo: Repository<BasicdataWastetypeEntity>;
+
+ @InjectEntityModel(BasicdataSorterEntity)
+ sorterRepo: Repository<BasicdataSorterEntity>;
+
+ @InjectEntityModel(BasicdataVehicleEntity)
+ vehicleRepo: Repository<BasicdataVehicleEntity>;
+
+ @InjectEntityModel(BasicdataSiteEntity)
+ siteRepo: Repository<BasicdataSiteEntity>;
+
+ @InjectEntityModel(RecycleOrderEntity)
+ orderRepo: Repository<RecycleOrderEntity>;
+
+ @InjectEntityModel(RecycleAppointmentEntity)
+ appointmentRepo: Repository<RecycleAppointmentEntity>;
+
+ @InjectEntityModel(RecycleTransactionEntity)
+ transRepo: Repository<RecycleTransactionEntity>;
+
+ @InjectEntityModel(UserInfoEntity)
+ userRepo: Repository<UserInfoEntity>;
+
+ @InjectEntityModel(PushVehicleGpsEntity)
+ vehicleGpsRepo: Repository<PushVehicleGpsEntity>;
+
+ async query(range: string) {
+ const unavailable: string[] = [];
+ const dates = this.getRange(range || 'month');
+ const today = moment().format('YYYY-MM-DD');
+ const yearStart = moment().startOf('year').format('YYYY-MM-DD');
+ const yearEnd = moment().format('YYYY-MM-DD');
+
+ const deptIds = this.deptIds();
+ if (deptIds && deptIds.length === 0) {
+ unavailable.push('褰撳墠璐﹀彿娌℃湁閮ㄩ棬鏁版嵁鏉冮檺');
+ }
+
+ const safe = async <T>(label: string, fn: () => Promise<T>, fallback: T): Promise<T> => {
+ try {
+ return await fn();
+ } catch (e: any) {
+ unavailable.push(`${label}锛�${e?.message || '鏌ヨ澶辫触'}`);
+ return fallback;
+ }
+ };
+
+ const emptySum = {};
+ const [period, todayRow, totalRow, yearRow] = await Promise.all([
+ safe('鍖洪棿绔欑偣鏃ユ眹鎬�', () => this.sumSite(dates.start, dates.end), emptySum),
+ safe('浠婃棩绔欑偣鏃ユ眹鎬�', () => this.sumSite(today, today), emptySum),
+ safe('绱绔欑偣鏃ユ眹鎬�', () => this.sumSite('1970-01-01', today), emptySum),
+ safe('骞村害绔欑偣鏃ユ眹鎬�', () => this.sumSite(yearStart, yearEnd), emptySum),
+ ]);
+
+ const wasteNames = await safe('鍨冨溇绉嶇被鍚嶇О', () => this.wasteNameMap(), new Map<string, string>());
+ const wasteMix = this.buildWasteMix(period, wasteNames);
+ const recycleItems = this.buildRecycleItems(period, wasteNames);
+ const { axis, trend } = await safe(
+ '鍥炴敹瓒嬪娍',
+ () => this.buildTrend(range || 'month', dates),
+ { axis: [], trend: { weight: [], value: [], carbon: [] } }
+ );
+ const yearCarbon = await safe('骞村害纰冲噺鎺�', () => this.buildYearCarbon(), Array.from({ length: 12 }, () => 0));
+ const ranks = await safe(
+ '鐝骇/涓撲笟/闄㈢郴鎺掕',
+ () => this.buildOrgRanks(dates.startTime, dates.endTime, unavailable),
+ { classRank: [], majorRank: [], collegeRank: [] }
+ );
+ const users = await safe(
+ '甯堢敓鍙備笌涓庣⒊绉垎',
+ () => this.buildUserPoints(range || 'month', dates, axis, unavailable),
+ {
+ totalParticipants: 0,
+ newParticipants: 0,
+ participateRate: 0,
+ greenActions: 0,
+ issued: 0,
+ consumed: 0,
+ transfer: 0,
+ donate: 0,
+ mall: 0,
+ balance: 0,
+ participateTrend: axis.map(() => 0),
+ deliverTrend: axis.map(() => 0),
+ }
+ );
+ const ops = await safe('杩愯惀鐩戞帶', () => this.buildOperation(today, dates, unavailable), {
+ sorterOnline: 0,
+ sorterTotal: 0,
+ vehicleOnline: 0,
+ vehicleTotal: 0,
+ siteOnline: 0,
+ siteTotal: 0,
+ todayOrders: 0,
+ todayAppointments: 0,
+ pendingAppointments: 0,
+ alerts: 0,
+ healthAlerts: 0,
+ ordersByType: {
+ axis: [],
+ '鐜板満鍥炴敹': [],
+ '棰勭害涓婇棬': [],
+ '澶у睆鎶曢��': [],
+ '娲诲姩鍥炴敹': [],
+ } as OrdersByType,
+ appointmentFlow: [],
+ sorterStatus: [],
+ vehicles: [],
+ sites: [],
+ });
+
+ const weightKg = n(period.weightTotal);
+ const recyclableKg = n(period.weightSw62);
+ const carbonKg = n(period.carbonTotal);
+ const totalCarbonKg = n(totalRow.carbonTotal);
+
+ return {
+ range: range || 'month',
+ axis,
+ kpis: {
+ todayWeightKg: round(n(todayRow.weightTotal), 1),
+ periodWeightTon: round(weightKg / 1000, 2),
+ resourceRate: weightKg > 0 ? round((recyclableKg / weightKg) * 100, 1) : 0,
+ periodValue: round(n(period.estimatedSalesSw62), 0),
+ periodCarbonTon: round(carbonKg / 1000, 2),
+ totalWeightTon: round(n(totalRow.weightTotal) / 1000, 1),
+ totalParticipants: users.totalParticipants,
+ totalPoints: users.balance,
+ recyclableTon: round(recyclableKg / 1000, 2),
+ bulkTon: round(n(period.weightSw63) / 1000, 2),
+ elecTon: round(n(period.weightSw62006) / 1000, 2),
+ fabricTon: round(n(period.weightSw62004) / 1000, 2),
+ yearCarbonTon: round(n(yearRow.carbonTotal) / 1000, 1),
+ monthCarbonTon: round(carbonKg / 1000, 2),
+ totalCarbonTon: round(totalCarbonKg / 1000, 1),
+ greenActions: users.greenActions,
+ newParticipants: users.newParticipants,
+ participateRate: users.participateRate,
+ pointsIssued: users.issued,
+ pointsConsumed: users.consumed,
+ pointsBalance: users.balance,
+ sorterOnline: ops.sorterOnline,
+ sorterTotal: ops.sorterTotal,
+ vehicleOnline: ops.vehicleOnline,
+ vehicleTotal: ops.vehicleTotal,
+ siteOnline: ops.siteOnline,
+ siteTotal: ops.siteTotal,
+ todayOrders: ops.todayOrders,
+ todayAppointments: ops.todayAppointments,
+ pendingAppointments: ops.pendingAppointments,
+ alerts: ops.alerts,
+ healthAlerts: ops.healthAlerts,
+ },
+ wasteMix,
+ recycleItems,
+ trend,
+ yearCarbon,
+ deptRank: ranks.collegeRank,
+ classRank: ranks.classRank,
+ majorRank: ranks.majorRank,
+ pointFlow: {
+ issued: users.issued,
+ transfer: users.transfer,
+ donate: users.donate,
+ mall: users.mall,
+ balance: users.balance,
+ },
+ participateTrend: users.participateTrend,
+ deliverTrend: users.deliverTrend,
+ ordersByType: ops.ordersByType,
+ appointmentFlow: ops.appointmentFlow,
+ sorterStatus: ops.sorterStatus,
+ vehicles: ops.vehicles,
+ sites: ops.sites,
+ unavailable,
+ };
+ }
+
+ 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;
+ }
+
+ /** 缂栧埗浜烘暟锛歜ase_sys_param.keyName = student.count.{departmentId} */
+ private async studentCount(unavailable: string[]) {
+ const ids = this.deptIds();
+ if (ids !== null && ids.length === 0) return 0;
+ const keys =
+ ids === null
+ ? (
+ await this.paramRepo
+ .createQueryBuilder('p')
+ .select('p.keyName', 'keyName')
+ .addSelect('p.data', 'data')
+ .where("p.keyName LIKE :prefix", { prefix: 'student.count.%' })
+ .getRawMany()
+ ).map(r => ({ key: r.keyName, data: r.data }))
+ : await Promise.all(
+ ids.map(async id => {
+ const row = await this.paramRepo.findOneBy({ keyName: `student.count.${id}` });
+ return { key: `student.count.${id}`, data: row?.data };
+ })
+ );
+
+ const missing = keys.filter(e => e.data === undefined || e.data === null || e.data === '');
+ const total = keys.reduce((s, e) => s + n(e.data), 0);
+ if (!keys.length || missing.length === keys.length) {
+ unavailable.push('鏈厤缃紪鍒朵汉鏁板弬鏁� student.count.{departmentId}锛屽弬涓庣巼鏆備负 0');
+ return 0;
+ }
+ if (missing.length) {
+ unavailable.push(`缂哄皯缂栧埗浜烘暟鍙傛暟锛�${missing.map(e => e.key).join('銆�')}`);
+ }
+ return total;
+ }
+
+ private getRange(range: string) {
+ const now = moment();
+ let startM = now.clone().startOf('month');
+ if (range === 'today') startM = now.clone().startOf('day');
+ else if (range === 'week') startM = now.clone().startOf('isoWeek');
+ else if (range === 'quarter') startM = now.clone().startOf('quarter' as any);
+ else if (range === 'year') startM = now.clone().startOf('year');
+
+ return {
+ start: startM.format('YYYY-MM-DD'),
+ end: now.format('YYYY-MM-DD'),
+ startTime: startM.format('YYYY-MM-DD HH:mm:ss'),
+ endTime: now.clone().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
+ };
+ }
+
+ private async sumSite(start: string, end: string) {
+ const qb = this.dailySite
+ .createQueryBuilder('a')
+ .select('SUM(a.weightTotal)', 'weightTotal')
+ .addSelect('SUM(a.weightSw60)', 'weightSw60')
+ .addSelect('SUM(a.weightSw61)', 'weightSw61')
+ .addSelect('SUM(a.weightSw62)', 'weightSw62')
+ .addSelect('SUM(a.weightSw63)', 'weightSw63')
+ .addSelect('SUM(a.weightSw64)', 'weightSw64')
+ .addSelect('SUM(a.timeTotal)', 'timeTotal')
+ .addSelect('SUM(a.timeSw60)', 'timeSw60')
+ .addSelect('SUM(a.timeSw61)', 'timeSw61')
+ .addSelect('SUM(a.timeSw62)', 'timeSw62')
+ .addSelect('SUM(a.timeSw63)', 'timeSw63')
+ .addSelect('SUM(a.timeSw64)', 'timeSw64')
+ .addSelect('SUM(a.carbonTotal)', 'carbonTotal')
+ .addSelect('SUM(a.estimatedSalesSw62)', 'estimatedSalesSw62');
+
+ for (const i of ['001', '002', '003', '004', '005', '006', '007']) {
+ qb.addSelect(`SUM(a.weightSw62${i})`, `weightSw62${i}`);
+ qb.addSelect(`SUM(a.timeSw62${i})`, `timeSw62${i}`);
+ qb.addSelect(`SUM(a.carbonSw62${i})`, `carbonSw62${i}`);
+ qb.addSelect(`SUM(a.estimatedSalesSw62${i})`, `estimatedSalesSw62${i}`);
+ }
+
+ qb.where('a.date BETWEEN :start AND :end', { start, end });
+ this.applyDept(qb, 'a');
+ return (await qb.getRawOne()) || {};
+ }
+
+ private async wasteNameMap() {
+ const list = await this.wasteTypeRepo.find({ select: ['code', 'name'] });
+ const map = new Map<string, string>();
+ list.forEach(e => map.set(String(e.code).toUpperCase(), e.name));
+ return map;
+ }
+
+ private buildWasteMix(row: any, names: Map<string, string>) {
+ const items = [
+ { code: 'SW62', key: 'Sw62' },
+ { code: 'SW64', key: 'Sw64' },
+ { code: 'SW63', key: 'Sw63' },
+ { code: 'SW61', key: 'Sw61' },
+ { code: 'SW60', key: 'Sw60' },
+ ];
+ return items.map(e => ({
+ name: names.get(e.code) || this.defaultWasteName(e.code),
+ code: e.code,
+ weight: round(n(row[`weight${e.key}`]), 1),
+ value: e.code === 'SW62' ? round(n(row.estimatedSalesSw62), 0) : 0,
+ count: round(n(row[`time${e.key}`]), 0),
+ color: WASTE_COLORS[e.code],
+ }));
+ }
+
+ private defaultWasteName(code: string) {
+ return (
+ {
+ SW60: '婀垮瀮鍦�',
+ SW61: '鏈夊鍨冨溇',
+ SW62: '鍙洖鏀剁墿',
+ SW63: '澶т欢鍨冨溇',
+ SW64: '鍏朵粬鍨冨溇',
+ }[code] || code
+ );
+ }
+
+ private buildRecycleItems(row: any, names: Map<string, string>) {
+ const codes = [
+ '900-001-S62',
+ '900-002-S62',
+ '900-003-S62',
+ '900-004-S62',
+ '900-005-S62',
+ '900-006-S62',
+ '900-007-S62',
+ ];
+ const fallback = ['搴熺焊', '濉戞枡', '閲戝睘', '缁囩墿', '鐜荤拑', '鐢靛瓙浜у搧', '鍏朵粬鍙洖鏀�'];
+ return codes.map((code, i) => {
+ const idx = String(i + 1).padStart(3, '0');
+ return {
+ name: names.get(code.toUpperCase()) || names.get(code) || fallback[i],
+ code,
+ weight: round(n(row[`weightSw62${idx}`]), 1),
+ value: round(n(row[`estimatedSalesSw62${idx}`]), 0),
+ carbon: round(n(row[`carbonSw62${idx}`]), 1),
+ color: ITEM_COLORS[i],
+ };
+ });
+ }
+
+ private async buildTrend(range: string, dates: { start: string; end: string; startTime: string; endTime: string }) {
+ const trendStart =
+ range === 'today' ? moment().subtract(6, 'day').format('YYYY-MM-DD') : dates.start;
+ const trendEnd = dates.end;
+
+ const qb = this.dailySite
+ .createQueryBuilder('a')
+ .select('a.date', 'date')
+ .addSelect('SUM(a.weightTotal)', 'weight')
+ .addSelect('SUM(a.estimatedSalesSw62)', 'value')
+ .addSelect('SUM(a.carbonTotal)', 'carbon')
+ .where('a.date BETWEEN :start AND :end', { start: trendStart, end: trendEnd })
+ .groupBy('a.date')
+ .orderBy('a.date', 'ASC');
+ this.applyDept(qb, 'a');
+ const rows = await qb.getRawMany();
+ const map = new Map(
+ rows.map(r => [
+ moment(r.date).format('YYYY-MM-DD'),
+ { weight: n(r.weight), value: n(r.value), carbon: n(r.carbon) },
+ ])
+ );
+
+ const axis: string[] = [];
+ const weight: number[] = [];
+ const value: number[] = [];
+ const carbon: number[] = [];
+
+ if (range === 'year') {
+ for (let m = 1; m <= 12; m++) {
+ axis.push(`${m}鏈坄);
+ const prefix = `${moment().year()}-${String(m).padStart(2, '0')}`;
+ let w = 0,
+ v = 0,
+ c = 0;
+ map.forEach((item, key) => {
+ if (key.startsWith(prefix)) {
+ w += item.weight;
+ v += item.value;
+ c += item.carbon;
+ }
+ });
+ weight.push(round(w, 1));
+ value.push(round(v, 0));
+ carbon.push(round(c, 1));
+ }
+ } else {
+ const cursor = moment(trendStart);
+ const end = moment(trendEnd);
+ while (cursor.isSameOrBefore(end, 'day')) {
+ const key = cursor.format('YYYY-MM-DD');
+ axis.push(cursor.format('MM-DD'));
+ const item = map.get(key) || { weight: 0, value: 0, carbon: 0 };
+ weight.push(round(item.weight, 1));
+ value.push(round(item.value, 0));
+ carbon.push(round(item.carbon, 1));
+ cursor.add(1, 'day');
+ }
+ }
+
+ return { axis, trend: { weight, value, carbon } };
+ }
+
+ private async buildYearCarbon() {
+ const start = moment().startOf('year').format('YYYY-MM-DD');
+ const end = moment().endOf('year').format('YYYY-MM-DD');
+ const qb = this.dailySite
+ .createQueryBuilder('a')
+ .select('MONTH(a.date)', 'm')
+ .addSelect('SUM(a.carbonTotal)', 'carbon')
+ .where('a.date BETWEEN :start AND :end', { start, end })
+ .groupBy('MONTH(a.date)');
+ this.applyDept(qb, 'a');
+ const rows = await qb.getRawMany();
+ const map = new Map(rows.map(r => [Number(r.m), n(r.carbon)]));
+ return Array.from({ length: 12 }, (_, i) => round((map.get(i + 1) || 0) / 1000, 3));
+ }
+
+ private async buildOrgRanks(startTime: string, endTime: string, unavailable: string[]) {
+ const empty = { classRank: [], majorRank: [], collegeRank: [] };
+ try {
+ const joinOrder = () => {
+ const qb = this.orderRepo
+ .createQueryBuilder('o')
+ .innerJoin(UserInfoEntity, 'u', 'o.campusUserId = u.unionid')
+ .where('o.status = :status', { status: '宸插畬鎴�' })
+ .andWhere('o.finishTime BETWEEN :start AND :end', { start: startTime, end: endTime });
+ this.applyDept(qb, 'o');
+ return qb;
+ };
+
+ const metrics = (qb: any) =>
+ qb
+ .addSelect('COUNT(DISTINCT o.campusUserId)', 'people')
+ .addSelect('SUM(o.totalWeight)', 'weight')
+ .addSelect('SUM(o.totalCarbonPoint)', 'points');
+
+ const classQb = metrics(
+ joinOrder()
+ .select('u.className', 'className')
+ .addSelect('u.collegeName', 'collegeName')
+ .andWhere("u.className IS NOT NULL AND u.className <> ''")
+ .groupBy('u.classId')
+ .addGroupBy('u.className')
+ .addGroupBy('u.collegeName')
+ .orderBy('people', 'DESC')
+ .limit(10)
+ );
+
+ const majorQb = metrics(
+ joinOrder()
+ .select('u.majorName', 'name')
+ .andWhere("u.majorName IS NOT NULL AND u.majorName <> ''")
+ .groupBy('u.majorId')
+ .addGroupBy('u.majorName')
+ .orderBy('people', 'DESC')
+ .limit(10)
+ );
+
+ const collegeQb = metrics(
+ joinOrder()
+ .select('u.collegeName', 'name')
+ .andWhere("u.collegeName IS NOT NULL AND u.collegeName <> ''")
+ .groupBy('u.collegeName')
+ .orderBy('people', 'DESC')
+ .limit(10)
+ );
+
+ const [classRows, majorRows, collegeRows] = await Promise.all([
+ classQb.getRawMany(),
+ majorQb.getRawMany(),
+ collegeQb.getRawMany(),
+ ]);
+
+ if (!classRows.length) {
+ unavailable.push('鐝骇鎺掕鏆傛棤鏁版嵁锛坈lassName 涓虹┖鎴栬鍗曟湭鍏宠仈鐢ㄦ埛锛�');
+ }
+ if (!majorRows.length) {
+ unavailable.push('涓撲笟鎺掕鏆傛棤鏁版嵁锛坢ajorName 涓虹┖鎴栬鍗曟湭鍏宠仈鐢ㄦ埛锛�');
+ }
+ if (!collegeRows.length) {
+ unavailable.push('闄㈢郴鎺掕鏆傛棤鏁版嵁锛坈ollegeName 涓虹┖鎴栬鍗曟湭鍏宠仈鐢ㄦ埛锛�');
+ }
+
+ const toRank = (name: string, r: any, org?: string) => {
+ const people = n(r.people) || 1;
+ const weight = n(r.weight);
+ const points = n(r.points);
+ return {
+ name,
+ org,
+ weight: round(weight, 1),
+ people: n(r.people),
+ rate: 0,
+ points: round(points, 0),
+ carbon: round(weight, 1),
+ size: n(r.people),
+ weightPerCapita: round(weight / people, 2),
+ pointPerCapita: round(points / people, 1),
+ };
+ };
+
+ return {
+ classRank: classRows.map(r => toRank(r.className, r, r.collegeName)),
+ majorRank: majorRows.map(r => toRank(r.name, r)),
+ collegeRank: collegeRows.map(r => toRank(r.name, r)),
+ };
+ } catch (e) {
+ unavailable.push('甯堢敓缁勭粐鎺掕鏌ヨ澶辫触锛岃妫�鏌ュ洖鏀惰鍗曚笌鏍″洯鐢ㄦ埛琛�');
+ return empty;
+ }
+ }
+
+ private async buildUserPoints(
+ range: string,
+ dates: { start: string; end: string; startTime: string; endTime: string },
+ axis: string[],
+ unavailable: string[]
+ ) {
+ const result = {
+ totalParticipants: 0,
+ newParticipants: 0,
+ participateRate: 0,
+ greenActions: 0,
+ issued: 0,
+ consumed: 0,
+ transfer: 0,
+ donate: 0,
+ mall: 0,
+ balance: 0,
+ participateTrend: [] as number[],
+ deliverTrend: [] as number[],
+ };
+
+ try {
+ const partQb = this.orderRepo
+ .createQueryBuilder('o')
+ .select('COUNT(DISTINCT o.campusUserId)', 'c')
+ .where('o.status = :status', { status: '宸插畬鎴�' });
+ this.applyDept(partQb, 'o');
+ result.totalParticipants = n((await partQb.getRawOne())?.c);
+
+ const newQb = partQb.clone().andWhere('o.finishTime BETWEEN :start AND :end', {
+ start: dates.startTime,
+ end: dates.endTime,
+ });
+ result.newParticipants = n((await newQb.getRawOne())?.c);
+
+ const userTotal = await this.studentCount(unavailable);
+ result.participateRate = userTotal > 0 ? round((result.totalParticipants / userTotal) * 100, 1) : 0;
+
+ const actQb = this.orderRepo
+ .createQueryBuilder('o')
+ .select('COUNT(o.id)', 'c')
+ .where('o.status = :status', { status: '宸插畬鎴�' });
+ this.applyDept(actQb, 'o');
+ result.greenActions = n((await actQb.getRawOne())?.c);
+
+ const transQb = this.transRepo
+ .createQueryBuilder('t')
+ .select('t.sourceType', 'sourceType')
+ .addSelect('t.type', 'type')
+ .addSelect('SUM(t.amount)', 'amount')
+ .where('t.createTime BETWEEN :start AND :end', { start: dates.startTime, end: dates.endTime })
+ .groupBy('t.sourceType')
+ .addGroupBy('t.type');
+ this.applyDept(transQb, 't');
+ const transRows = await transQb.getRawMany();
+ transRows.forEach(r => {
+ const amount = n(r.amount);
+ if (r.sourceType === '鍥炴敹' || r.sourceType === '绛惧埌') result.issued += amount;
+ if (r.sourceType === '鍟嗗煄') result.mall += amount;
+ if (r.sourceType === '璧犻��') result.transfer += amount;
+ if (r.sourceType === '鎹愯禒') result.donate += amount;
+ if (r.type === '鏀嚭') result.consumed += amount;
+ });
+
+ const balQb = this.userRepo.createQueryBuilder('u').select('SUM(u.carbonBalance)', 'b');
+ this.applyDept(balQb, 'u');
+ result.balance = round(n((await balQb.getRawOne())?.b), 0);
+
+ const trendStart =
+ range === 'today'
+ ? moment().subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss')
+ : dates.startTime;
+ const trendQb = this.orderRepo
+ .createQueryBuilder('o')
+ .select(range === 'year' ? 'MONTH(o.finishTime)' : 'DATE(o.finishTime)', 'd')
+ .addSelect('COUNT(DISTINCT o.campusUserId)', 'people')
+ .addSelect('COUNT(o.id)', 'times')
+ .where('o.status = :status', { status: '宸插畬鎴�' })
+ .andWhere('o.finishTime BETWEEN :start AND :end', {
+ start: trendStart,
+ end: dates.endTime,
+ })
+ .groupBy(range === 'year' ? 'MONTH(o.finishTime)' : 'DATE(o.finishTime)')
+ .orderBy('d', 'ASC');
+ this.applyDept(trendQb, 'o');
+ const trendRows = await trendQb.getRawMany();
+ const peopleMap = new Map<string, number>();
+ const timesMap = new Map<string, number>();
+ trendRows.forEach(r => {
+ const key = range === 'year' ? `${Number(r.d)}鏈坄 : moment(r.d).format('MM-DD');
+ peopleMap.set(key, n(r.people));
+ timesMap.set(key, n(r.times));
+ });
+ result.participateTrend = axis.map(k => peopleMap.get(k) || 0);
+ result.deliverTrend = axis.map(k => timesMap.get(k) || 0);
+ } catch (e) {
+ unavailable.push('甯堢敓鍙備笌 / 纰崇Н鍒嗘棤娉曚粠鐢ㄦ埛銆佽鍗曘�佹祦姘磋〃姹囨��');
+ }
+
+ return result;
+ }
+
+ private async buildOperation(today: string, _dates: any, unavailable: string[]) {
+ const start15 = moment().subtract(15, 'minute').format('YYYY-MM-DD HH:mm:ss');
+ const todayStart = moment().startOf('day').format('YYYY-MM-DD HH:mm:ss');
+ const todayEnd = moment().endOf('day').format('YYYY-MM-DD HH:mm:ss');
+
+ const sorterQb = this.sorterRepo.createQueryBuilder('a').select('COUNT(a.id)', 'c');
+ this.applyDept(sorterQb, 'a');
+ const sorterTotal = n((await sorterQb.getRawOne())?.c);
+
+ const dsQb = this.dailySorter.createQueryBuilder('a').where('a.date = :today', { today });
+ this.applyDept(dsQb, 'a');
+ const dsRows = await dsQb.getMany();
+ if (!dsRows.length) {
+ unavailable.push('鍒嗘嫞鍛樻棩琛ㄦ殏鏃犲綋鏃ユ暟鎹紝鐘舵�佹寜绂荤嚎缁熻');
+ }
+
+ let working = 0;
+ let resting = 0;
+ let abnormal = 0;
+ let offlineFromTable = 0;
+ let alerts = 0;
+ let healthAlerts = 0;
+ dsRows.forEach(r => {
+ alerts += n(r.unhandledAlarmCount);
+ healthAlerts += n(r.healthAbnormalCount);
+ if (n(r.healthAbnormalCount) > 0 || n(r.unhandledAlarmCount) > 0) {
+ abnormal += 1;
+ } else if (r.checkInTime && !r.checkOutTime) {
+ working += 1;
+ } else if (r.checkInTime && r.checkOutTime) {
+ resting += 1;
+ } else {
+ offlineFromTable += 1;
+ }
+ });
+ const sorterOnline = working;
+ const offline = offlineFromTable + Math.max(sorterTotal - dsRows.length, 0);
+ const sorterStatus = [
+ { name: '宸ヤ綔涓�', value: working, color: '#22c55e' },
+ { name: '浼戞伅', value: resting, color: '#f59e0b' },
+ { name: '寮傚父', value: abnormal, color: '#ef4444' },
+ { name: '绂荤嚎', value: offline, color: '#94a3b8' },
+ ];
+
+ const vehicleQb = this.vehicleRepo.createQueryBuilder('a').select('COUNT(a.id)', 'c');
+ this.applyDept(vehicleQb, 'a');
+ const vehicleTotal = n((await vehicleQb.getRawOne())?.c);
+
+ const vGpsQb = this.vehicleGpsRepo
+ .createQueryBuilder('a')
+ .select('COUNT(DISTINCT a.vehicleNo)', 'c')
+ .where('a.gpsTime >= :start15', { start15 });
+ this.applyDept(vGpsQb, 'a');
+ const vehicleOnline = n((await vGpsQb.getRawOne())?.c);
+
+ const siteQb = this.siteRepo
+ .createQueryBuilder('a')
+ .select('COUNT(a.id)', 'c')
+ .where("a.siteType = '鏀堕泦鐐�'");
+ this.applyDept(siteQb, 'a');
+ const siteTotal = n((await siteQb.getRawOne())?.c);
+ const siteOnlineQb = siteQb.clone().andWhere('a.status = :st', { st: 1 });
+ const siteOnline = n((await siteOnlineQb.getRawOne())?.c);
+
+ const orderQb = this.orderRepo
+ .createQueryBuilder('o')
+ .select('COUNT(o.id)', 'c')
+ .where('o.createTime BETWEEN :start AND :end', { start: todayStart, end: todayEnd });
+ this.applyDept(orderQb, 'o');
+ const todayOrders = n((await orderQb.getRawOne())?.c);
+
+ const apptQb = this.appointmentRepo
+ .createQueryBuilder('a')
+ .select('a.status', 'status')
+ .addSelect('COUNT(a.id)', 'c')
+ .where('a.appointmentDate = :today', { today })
+ .groupBy('a.status');
+ this.applyDept(apptQb, 'a');
+ const apptRows = await apptQb.getRawMany();
+ const apptMap = new Map(apptRows.map(r => [r.status, n(r.c)]));
+ const todayAppointments = apptRows.reduce((s, r) => s + n(r.c), 0);
+ const pendingAppointments =
+ n(apptMap.get('鏂板')) +
+ n(apptMap.get('寰呭鏍�')) +
+ n(apptMap.get('寰呮帴鍗�')) +
+ n(apptMap.get('宸茬‘璁�'));
+
+ const appointmentFlow = [
+ { name: '棰勭害', value: n(apptMap.get('鏂板')) + n(apptMap.get('寰呭鏍�')) },
+ { name: '鎺ュ崟', value: n(apptMap.get('宸茬‘璁�')) + n(apptMap.get('宸叉帴鍗�')) },
+ { name: '瀹屾垚', value: n(apptMap.get('宸插畬鎴�')) },
+ ];
+
+ const typeQb = this.orderRepo
+ .createQueryBuilder('o')
+ .select('DATE(o.createTime)', 'd')
+ .addSelect('o.orderType', 'orderType')
+ .addSelect('COUNT(o.id)', 'c')
+ .where('o.createTime BETWEEN :start AND :end', {
+ start: moment().subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+ end: todayEnd,
+ })
+ .groupBy('DATE(o.createTime)')
+ .addGroupBy('o.orderType');
+ this.applyDept(typeQb, 'o');
+ const typeRows = await typeQb.getRawMany();
+ const days = Array.from({ length: 7 }, (_, i) => moment().subtract(6 - i, 'day').format('YYYY-MM-DD'));
+ const typeMap: Omit<OrdersByType, 'axis'> = {
+ '鐜板満鍥炴敹': days.map(() => 0),
+ '棰勭害涓婇棬': days.map(() => 0),
+ '澶у睆鎶曢��': days.map(() => 0),
+ '娲诲姩鍥炴敹': days.map(() => 0),
+ };
+ const typeAlias: Record<string, keyof Omit<OrdersByType, 'axis'>> = {
+ '鐜板満': '鐜板満鍥炴敹',
+ '棰勭害': '棰勭害涓婇棬',
+ '澶у睆': '澶у睆鎶曢��',
+ '娲诲姩': '娲诲姩鍥炴敹',
+ };
+ typeRows.forEach(r => {
+ const key = typeAlias[r.orderType];
+ const idx = days.indexOf(moment(r.d).format('YYYY-MM-DD'));
+ if (idx >= 0 && key) typeMap[key][idx] = n(r.c);
+ });
+
+ const vQb = this.dailyVehicle
+ .createQueryBuilder('a')
+ .select('a.vehicleNo', 'vehicleNo')
+ .addSelect('a.totalKm', 'km')
+ .addSelect('a.minutesAbnormalIdle', 'idle')
+ .addSelect('a.overspeedCount', 'overspeed')
+ .where('a.date = :today', { today })
+ .orderBy('a.totalKm', 'DESC')
+ .limit(8);
+ this.applyDept(vQb, 'a');
+ const vRows = await vQb.getRawMany();
+ const onlineSet = new Set(
+ (
+ await this.vehicleGpsRepo
+ .createQueryBuilder('a')
+ .select('DISTINCT a.vehicleNo', 'vehicleNo')
+ .where('a.gpsTime >= :start15', { start15 })
+ .getRawMany()
+ ).map(r => r.vehicleNo)
+ );
+ const vehicles = vRows.map(r => ({
+ no: r.vehicleNo,
+ km: round(n(r.km), 1),
+ idle: n(r.idle),
+ overspeed: n(r.overspeed),
+ status: onlineSet.has(r.vehicleNo) ? '鍦ㄧ嚎' : '绂荤嚎',
+ }));
+
+ const sQb = this.dailySite
+ .createQueryBuilder('a')
+ .select('a.siteName', 'name')
+ .addSelect('SUM(a.weightTotal)', 'weight')
+ .where('a.date = :today', { today })
+ .groupBy('a.siteName')
+ .orderBy('weight', 'DESC')
+ .limit(8);
+ this.applyDept(sQb, 'a');
+ const sRows = await sQb.getRawMany();
+ const sites = sRows.map(r => ({
+ name: r.name,
+ weight: round(n(r.weight), 1),
+ online: true,
+ }));
+
+ return {
+ sorterOnline,
+ sorterTotal,
+ vehicleOnline,
+ vehicleTotal,
+ siteOnline,
+ siteTotal,
+ todayOrders,
+ todayAppointments,
+ pendingAppointments,
+ alerts,
+ healthAlerts,
+ ordersByType: {
+ axis: days.map(d => d.slice(5)),
+ ...typeMap,
+ } as OrdersByType,
+ appointmentFlow,
+ sorterStatus,
+ vehicles,
+ sites,
+ };
+ }
+}
diff --git a/src/modules/report/service/taskservice.ts b/src/modules/report/service/taskservice.ts
index 56c3a21..4498ccb 100644
--- a/src/modules/report/service/taskservice.ts
+++ b/src/modules/report/service/taskservice.ts
@@ -67,10 +67,12 @@
{ date: yesterdayStr, type: 'SITE_DAILY', isToday: false },
{ date: yesterdayStr, type: 'DEPT_DAILY', isToday: false },
{ date: yesterdayStr, type: 'VEHICLE_DAILY', isToday: false }, // 鏂板
+ { date: yesterdayStr, type: 'SORTER_DAILY', isToday: false }, // 鏂板
{ date: todayStr, type: 'SITE_DAILY', isToday: true },
{ date: todayStr, type: 'DEPT_DAILY', isToday: true },
{ date: todayStr, type: 'VEHICLE_DAILY', isToday: true }, // 鏂板
+ { date: todayStr, type: 'SORTER_DAILY', isToday: true }, // 鏂板
];
diff --git a/src/modules/report/service/taskservicesorter.ts b/src/modules/report/service/taskservicesorter.ts
new file mode 100644
index 0000000..c3fabb4
--- /dev/null
+++ b/src/modules/report/service/taskservicesorter.ts
@@ -0,0 +1,922 @@
+import { Provide } from '@midwayjs/core';
+import { BaseService } from '@cool-midway/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+
+import { ReportDailySorterEntity } from '../entity/dailysorter';
+
+import { BasicdataSorterEntity } from '../../basicdata/entity/sorter';
+
+import { PushSorterAttendanceEntity } from '../../push/entity/sorterattendance';
+import { PushSorterGpsEntity } from '../../push/entity/sortergps';
+import { PushSorterHealthEntity } from '../../push/entity/sorterhealth';
+import { PushSorterAlarmEntity } from '../../push/entity/sorteralarm';
+
+import { RecycleOrderEntity } from '../../shop/entity/recycleorder';
+
+
+/**
+ * 鍒嗘嫞鍛樻棩鎶ョ粺璁�
+ *
+ * 缁熻鍘熷垯锛�
+ *
+ * 1. BasicdataSorterEntity 鏄垎鎷e憳涓绘暟鎹�
+ * 2. businessId 鏄垎鎷e憳涓氬姟鍞竴鏍囪瘑
+ * 3. 涓�鍚嶅垎鎷e憳涓�澶╀竴鏉℃棩鎶�
+ * 4. date + businessId 鍞竴
+ *
+ * 鏁版嵁鏉ユ簮锛�
+ *
+ * BasicdataSorter
+ * 鈹溾攢鈹� Attendance
+ * 鈹溾攢鈹� GPS
+ * 鈹溾攢鈹� Health
+ * 鈹溾攢鈹� Alarm
+ * 鈹斺攢鈹� RecycleOrder
+ *
+ * 鈫�
+ *
+ * ReportDailySorter
+ */
+@Provide()
+export class TaskServiceSorter extends BaseService {
+
+ /**
+ * 鍒嗘嫞鍛樺熀纭�淇℃伅
+ */
+ @InjectEntityModel(BasicdataSorterEntity)
+ basicdataSorterEntity: Repository<BasicdataSorterEntity>;
+
+ /**
+ * 鍒嗘嫞鍛樻棩鎶�
+ */
+ @InjectEntityModel(ReportDailySorterEntity)
+ reportDailySorterEntity: Repository<ReportDailySorterEntity>;
+
+ /**
+ * 鍒嗘嫞鍛樿�冨嫟
+ */
+ @InjectEntityModel(PushSorterAttendanceEntity)
+ pushSorterAttendanceEntity: Repository<PushSorterAttendanceEntity>;
+
+ /**
+ * 鍒嗘嫞鍛楪PS
+ */
+ @InjectEntityModel(PushSorterGpsEntity)
+ pushSorterGpsEntity: Repository<PushSorterGpsEntity>;
+
+ /**
+ * 鍒嗘嫞鍛樺仴搴�
+ */
+ @InjectEntityModel(PushSorterHealthEntity)
+ pushSorterHealthEntity: Repository<PushSorterHealthEntity>;
+
+ /**
+ * 鍒嗘嫞鍛樺仴搴峰憡璀�
+ */
+ @InjectEntityModel(PushSorterAlarmEntity)
+ pushSorterAlarmEntity: Repository<PushSorterAlarmEntity>;
+
+ /**
+ * 鍥炴敹璁㈠崟
+ */
+ @InjectEntityModel(RecycleOrderEntity)
+ recycleOrderEntity: Repository<RecycleOrderEntity>;
+
+
+ /**
+ * =========================================================
+ * 姣忔棩缁熻
+ * =========================================================
+ *
+ * @param targetDate 缁熻鏃ユ湡锛屼緥濡傦細2026-08-16
+ */
+ async aggregateDailyData(targetDate: string) {
+
+ const startTime = `${targetDate} 00:00:00`;
+ const endTime = `${targetDate} 23:59:59`;
+
+ // =======================================================
+ // 1. 鏌ヨ褰撳ぉ鏈夋晥鐨勫垎鎷e憳
+ // =======================================================
+
+ const sorterList =
+ await this.getActiveSorters(targetDate);
+
+ if (!sorterList.length) {
+ return 0;
+ }
+
+
+ // =======================================================
+ // 2. 鍒濆鍖栨棩鎶�
+ //
+ // 娉ㄦ剰锛�
+ // 杩欓噷浠ュ垎鎷e憳鍩虹淇℃伅涓哄噯锛岃�屼笉鏄互褰撳ぉ浜х敓鐨勬暟鎹负鍑嗐��
+ //
+ // 鍗充娇鏌愪釜鍒嗘嫞鍛樺綋澶╂病鏈夛細
+ // GPS / 鍋ュ悍 / 璁㈠崟 / 鑰冨嫟
+ //
+ // 涔熶細浜х敓涓�鏉℃棩鎶ャ��
+ // =======================================================
+
+ const summaryMap =
+ new Map<string, Partial<ReportDailySorterEntity>>();
+
+ for (const sorter of sorterList) {
+
+ summaryMap.set(
+ sorter.businessId,
+ {
+ date: targetDate,
+
+ businessId: sorter.businessId,
+ businessName: sorter.businessName,
+
+ departmentId: sorter.departmentId,
+ workSiteId: sorter.workSiteId,
+
+ checkInTime: null,
+ checkOutTime: null,
+ workHours: 0,
+ attendanceStatus: '缂哄崱',
+
+ gpsPointCount: 0,
+ totalKm: 0,
+ minutesMoving: 0,
+
+ orderCount: 0,
+ completedOrderCount: 0,
+ appointmentOrderCount: 0,
+ totalCarbonPoint: 0,
+
+ healthDataCount: 0,
+ healthAbnormalCount: 0,
+
+ alarmCount: 0,
+ generalAlarmCount: 0,
+ mediumAlarmCount: 0,
+ seriousAlarmCount: 0,
+ unhandledAlarmCount: 0,
+ }
+ );
+ }
+
+
+ // =======================================================
+ // 3. 鑰冨嫟缁熻
+ // =======================================================
+
+ await this.aggregateAttendance(
+ targetDate,
+ summaryMap
+ );
+
+
+ // =======================================================
+ // 4. GPS缁熻
+ // =======================================================
+
+ await this.aggregateGps(
+ startTime,
+ endTime,
+ summaryMap
+ );
+
+
+ // =======================================================
+ // 5. 鍋ュ悍鏁版嵁缁熻
+ // =======================================================
+
+ await this.aggregateHealth(
+ startTime,
+ endTime,
+ summaryMap
+ );
+
+
+ // =======================================================
+ // 6. 鍋ュ悍鍛婅缁熻
+ // =======================================================
+
+ await this.aggregateAlarm(
+ startTime,
+ endTime,
+ summaryMap
+ );
+
+
+ // =======================================================
+ // 7. 鍥炴敹璁㈠崟缁熻
+ // =======================================================
+
+ await this.aggregateRecycleOrder(
+ startTime,
+ endTime,
+ sorterList,
+ summaryMap
+ );
+
+
+ // =======================================================
+ // 8. 淇濆瓨鏃ユ姤
+ // =======================================================
+
+ const records =
+ Array.from(summaryMap.values());
+
+ await this.upsertDailyReports(records);
+
+ return records.length;
+ }
+
+
+ /**
+ * =========================================================
+ * 鏌ヨ褰撳ぉ鏈夋晥鍒嗘嫞鍛�
+ * =========================================================
+ *
+ * 鐘舵�侊細
+ *
+ * 0 = 鍦ㄨ亴
+ * 1 = 绂昏亴
+ * 2 = 绂佺敤
+ *
+ * 鍚屾椂鑰冭檻锛�
+ *
+ * hireDate
+ * leaveDate
+ */
+ private async getActiveSorters(
+ targetDate: string,
+ ): Promise<BasicdataSorterEntity[]> {
+
+ return await this.basicdataSorterEntity
+ .createQueryBuilder('s')
+
+ .where('s.status = :status', {
+ status: 0,
+ })
+
+ .andWhere(
+ '(s.hireDate IS NULL OR s.hireDate <= :targetDate)',
+ {
+ targetDate,
+ }
+ )
+
+ .andWhere(
+ '(s.leaveDate IS NULL OR s.leaveDate >= :targetDate)',
+ {
+ targetDate,
+ }
+ )
+
+ .getMany();
+ }
+
+
+ /**
+ * =========================================================
+ * 鑰冨嫟缁熻
+ * =========================================================
+ */
+ private async aggregateAttendance(
+ targetDate: string,
+ summaryMap: Map<
+ string,
+ Partial<ReportDailySorterEntity>
+ >,
+ ) {
+
+ const list =
+ await this.pushSorterAttendanceEntity
+ .createQueryBuilder('a')
+ .where(
+ 'a.workDate = :targetDate',
+ {
+ targetDate,
+ }
+ )
+ .getMany();
+
+
+ for (const item of list) {
+
+ const summary =
+ summaryMap.get(item.idCard);
+
+ if (!summary) {
+ continue;
+ }
+
+
+ summary.checkInTime =
+ item.checkInTime || null;
+
+ summary.checkOutTime =
+ item.checkOutTime || null;
+
+ summary.workHours =
+ Number(item.workHours || 0);
+
+ summary.attendanceStatus =
+ item.status || '缂哄崱';
+ }
+ }
+
+
+ /**
+ * =========================================================
+ * GPS缁熻
+ * =========================================================
+ *
+ * 缁熻锛�
+ *
+ * gpsPointCount
+ * totalKm
+ * minutesMoving
+ *
+ * GPS榛樿鎸夌収涓婁紶鏃堕棿鍗囧簭鎺掑垪銆�
+ */
+ private async aggregateGps(
+ startTime: string,
+ endTime: string,
+ summaryMap: Map<
+ string,
+ Partial<ReportDailySorterEntity>
+ >,
+ ) {
+
+ const list =
+ await this.pushSorterGpsEntity
+ .createQueryBuilder('gps')
+
+ .select([
+ 'gps.idCard AS idCard',
+ 'gps.uploadTime AS uploadTime',
+ 'gps.longitude AS longitude',
+ 'gps.latitude AS latitude',
+ 'gps.speed AS speed',
+ ])
+
+ .where(
+ 'gps.uploadTime BETWEEN :startTime AND :endTime',
+ {
+ startTime,
+ endTime,
+ }
+ )
+
+ .orderBy('gps.idCard', 'ASC')
+ .addOrderBy('gps.uploadTime', 'ASC')
+
+ .getRawMany();
+
+
+ // -------------------------------------------------------
+ // 鎸変汉鍛樺垎缁�
+ // -------------------------------------------------------
+
+ const gpsMap =
+ new Map<string, any[]>();
+
+
+ for (const item of list) {
+
+ if (!item.idCard) {
+ continue;
+ }
+
+ // 鍙鐞嗗綋澶╂湁鏁堝垎鎷e憳
+ if (!summaryMap.has(item.idCard)) {
+ continue;
+ }
+
+
+ if (!gpsMap.has(item.idCard)) {
+
+ gpsMap.set(
+ item.idCard,
+ []
+ );
+ }
+
+
+ gpsMap
+ .get(item.idCard)!
+ .push(item);
+ }
+
+
+ // -------------------------------------------------------
+ // 璁$畻姣忎釜浜虹殑杞ㄨ抗
+ // -------------------------------------------------------
+
+ for (const [
+ businessId,
+ points,
+ ] of gpsMap.entries()) {
+
+ const summary =
+ summaryMap.get(businessId);
+
+ if (!summary) {
+ continue;
+ }
+
+
+ summary.gpsPointCount =
+ points.length;
+
+
+ let totalMeter = 0;
+
+ let movingSeconds = 0;
+
+
+ for (
+ let i = 1;
+ i < points.length;
+ i++
+ ) {
+
+ const prev =
+ points[i - 1];
+
+ const curr =
+ points[i];
+
+
+ const prevTime =
+ new Date(
+ prev.uploadTime
+ ).getTime();
+
+ const currTime =
+ new Date(
+ curr.uploadTime
+ ).getTime();
+
+
+ const diffSeconds =
+ (currTime - prevTime) / 1000;
+
+
+ // ---------------------------------------------------
+ // 寮傚父GPS鏃堕棿闂撮殧
+ //
+ // 姝e父閲囨牱绾�5鍒嗛挓
+ // 瓒呰繃10鍒嗛挓璁や负杩欎袱涓偣涓嶅簲璇ョ洿鎺ヨ繛绾�
+ // ---------------------------------------------------
+
+ if (
+ diffSeconds <= 0 ||
+ diffSeconds > 600
+ ) {
+ continue;
+ }
+
+
+ const speed =
+ Number(curr.speed || 0);
+
+
+ // ---------------------------------------------------
+ // speed > 2 km/h
+ // 璁や负鍒嗘嫞鍛樻鍦ㄧЩ鍔�
+ // ---------------------------------------------------
+
+ if (speed > 2) {
+
+ movingSeconds +=
+ diffSeconds;
+
+
+ // km/h 鈫� m/s 鈫� 绫�
+ const meter =
+ speed *
+ 1000 /
+ 3600 *
+ diffSeconds;
+
+
+ totalMeter +=
+ meter;
+ }
+ }
+
+
+ summary.totalKm =
+ Number(
+ (
+ totalMeter / 1000
+ ).toFixed(2)
+ );
+
+
+ summary.minutesMoving =
+ Math.round(
+ movingSeconds / 60
+ );
+ }
+ }
+
+
+ /**
+ * =========================================================
+ * 鍋ュ悍鏁版嵁缁熻
+ * =========================================================
+ *
+ * 涓嶅湪鏃ユ姤淇濆瓨锛�
+ *
+ * 浣撴俯
+ * 琛�鍘�
+ * 琛�姘�
+ * 蹇冪巼
+ *
+ * 鏃ユ姤鍙粺璁★細
+ *
+ * 鍋ュ悍鏁版嵁鏁伴噺
+ * 鍋ュ悍寮傚父鏁伴噺
+ */
+ private async aggregateHealth(
+ startTime: string,
+ endTime: string,
+ summaryMap: Map<
+ string,
+ Partial<ReportDailySorterEntity>
+ >,
+ ) {
+
+ const list =
+ await this.pushSorterHealthEntity
+ .createQueryBuilder('h')
+
+ .select([
+ 'h.idCard AS idCard',
+ 'h.dataType AS dataType',
+ ])
+
+ .where(
+ 'h.uploadTime BETWEEN :startTime AND :endTime',
+ {
+ startTime,
+ endTime,
+ }
+ )
+
+ .getRawMany();
+
+
+ for (const item of list) {
+
+ const summary =
+ summaryMap.get(item.idCard);
+
+ if (!summary) {
+ continue;
+ }
+
+
+ summary.healthDataCount =
+ Number(
+ summary.healthDataCount || 0
+ ) + 1;
+
+
+ if (
+ item.dataType === '寮傚父'
+ ) {
+
+ summary.healthAbnormalCount =
+ Number(
+ summary.healthAbnormalCount || 0
+ ) + 1;
+ }
+ }
+ }
+
+
+ /**
+ * =========================================================
+ * 鍋ュ悍鍛婅缁熻
+ * =========================================================
+ */
+ private async aggregateAlarm(
+ startTime: string,
+ endTime: string,
+ summaryMap: Map<
+ string,
+ Partial<ReportDailySorterEntity>
+ >,
+ ) {
+
+ const list =
+ await this.pushSorterAlarmEntity
+ .createQueryBuilder('a')
+
+ .select([
+ 'a.idCard AS idCard',
+ 'a.alarmLevel AS alarmLevel',
+ 'a.status AS status',
+ ])
+
+ .where(
+ 'a.uploadTime BETWEEN :startTime AND :endTime',
+ {
+ startTime,
+ endTime,
+ }
+ )
+
+ .getRawMany();
+
+
+ for (const item of list) {
+
+ const summary =
+ summaryMap.get(item.idCard);
+
+ if (!summary) {
+ continue;
+ }
+
+
+ // 鎬诲憡璀�
+ summary.alarmCount =
+ Number(
+ summary.alarmCount || 0
+ ) + 1;
+
+
+ // ---------------------------------------------------
+ // 鍛婅绛夌骇
+ // ---------------------------------------------------
+
+ switch (item.alarmLevel) {
+
+ case '涓�鑸�':
+
+ summary.generalAlarmCount =
+ Number(
+ summary.generalAlarmCount || 0
+ ) + 1;
+
+ break;
+
+
+ case '涓瓑':
+
+ summary.mediumAlarmCount =
+ Number(
+ summary.mediumAlarmCount || 0
+ ) + 1;
+
+ break;
+
+
+ case '涓ラ噸':
+
+ summary.seriousAlarmCount =
+ Number(
+ summary.seriousAlarmCount || 0
+ ) + 1;
+
+ break;
+ }
+
+
+ // ---------------------------------------------------
+ // status
+ //
+ // 0 = 鏈鐞�
+ // 1 = 宸插鐞�
+ // ---------------------------------------------------
+
+ if (
+ Number(item.status) === 0
+ ) {
+
+ summary.unhandledAlarmCount =
+ Number(
+ summary.unhandledAlarmCount || 0
+ ) + 1;
+ }
+ }
+ }
+
+
+ /**
+ * =========================================================
+ * 鍥炴敹璁㈠崟缁熻
+ * =========================================================
+ *
+ * RecycleOrder锛�
+ *
+ * sorterId
+ * 鈫�
+ * BasicdataSorter.id
+ * 鈫�
+ * businessId
+ *
+ * 缁熻鐨勬槸锛�
+ *
+ * 褰撳ぉ瀹屾垚/澶勭悊鐨勫洖鏀惰鍗�
+ *
+ * 鎵�浠ヤ娇鐢細
+ *
+ * finishTime
+ */
+ private async aggregateRecycleOrder(
+ startTime: string,
+ endTime: string,
+
+ sorterList: BasicdataSorterEntity[],
+
+ summaryMap: Map<
+ string,
+ Partial<ReportDailySorterEntity>
+ >,
+ ) {
+
+ const list =
+ await this.recycleOrderEntity
+ .createQueryBuilder('o')
+
+ .select([
+ 'o.sorterId AS sorterId',
+ 'o.orderType AS orderType',
+ 'o.status AS status',
+ 'o.carbonPoint AS carbonPoint',
+ ])
+
+ .where(
+ 'o.finishTime BETWEEN :startTime AND :endTime',
+ {
+ startTime,
+ endTime,
+ }
+ )
+
+ .andWhere(
+ 'o.sorterId IS NOT NULL'
+ )
+
+ .andWhere(
+ 'o.sorterId <> 0'
+ )
+
+ .getRawMany();
+
+
+ // -------------------------------------------------------
+ // 寤虹珛 sorterId 鈫� businessId 鏄犲皠
+ // -------------------------------------------------------
+
+ const sorterMap =
+ new Map<number, string>();
+
+
+ for (const sorter of sorterList) {
+
+ sorterMap.set(
+ Number(sorter.id),
+ sorter.businessId
+ );
+ }
+
+
+ // -------------------------------------------------------
+ // 缁熻璁㈠崟
+ // -------------------------------------------------------
+
+ for (const order of list) {
+
+ const sorterId =
+ Number(order.sorterId);
+
+
+ const businessId =
+ sorterMap.get(sorterId);
+
+
+ if (!businessId) {
+ continue;
+ }
+
+
+ const summary =
+ summaryMap.get(businessId);
+
+ if (!summary) {
+ continue;
+ }
+
+
+ // 鎬昏鍗曟暟
+ summary.orderCount =
+ Number(
+ summary.orderCount || 0
+ ) + 1;
+
+
+ // 瀹屾垚璁㈠崟
+ if (
+ order.status === '瀹屾垚'
+ ) {
+
+ summary.completedOrderCount =
+ Number(
+ summary.completedOrderCount || 0
+ ) + 1;
+ }
+
+
+ // 棰勭害涓婇棬璁㈠崟
+ if (
+ order.orderType === '棰勭害'
+ ) {
+
+ summary.appointmentOrderCount =
+ Number(
+ summary.appointmentOrderCount || 0
+ ) + 1;
+ }
+
+
+ // 鍙戞斁纰崇Н鍒�
+ summary.totalCarbonPoint =
+ Number(
+ summary.totalCarbonPoint || 0
+ ) +
+ Number(
+ order.carbonPoint || 0
+ );
+ }
+ }
+
+
+ /**
+ * =========================================================
+ * 鏃ユ姤 UPSERT
+ * =========================================================
+ *
+ * 鍞竴閿細
+ *
+ * date + businessId
+ *
+ * 鍥犳浠诲姟閲嶅鎵ц涓嶄細浜х敓閲嶅鏁版嵁銆�
+ */
+ private async upsertDailyReports(
+ records: Partial<ReportDailySorterEntity>[],
+ ) {
+
+ if (!records.length) {
+ return;
+ }
+
+
+ await this.reportDailySorterEntity
+ .createQueryBuilder()
+ .insert()
+ .into(ReportDailySorterEntity)
+ .values(records)
+ .orUpdate(
+ [
+ 'businessName',
+ 'departmentId',
+ 'workSiteId',
+
+ 'checkInTime',
+ 'checkOutTime',
+ 'workHours',
+ 'attendanceStatus',
+
+ 'gpsPointCount',
+ 'totalKm',
+ 'minutesMoving',
+
+ 'orderCount',
+ 'completedOrderCount',
+ 'appointmentOrderCount',
+ 'totalCarbonPoint',
+
+ 'healthDataCount',
+ 'healthAbnormalCount',
+
+ 'alarmCount',
+ 'generalAlarmCount',
+ 'mediumAlarmCount',
+ 'seriousAlarmCount',
+ 'unhandledAlarmCount',
+ ],
+ [
+ 'date',
+ 'businessId',
+ ],
+ )
+ .execute();
+ }
+
+}
\ No newline at end of file
diff --git a/src/modules/shop/controller/app/sorter.ts b/src/modules/shop/controller/app/sorter.ts
new file mode 100644
index 0000000..d6db0d9
--- /dev/null
+++ b/src/modules/shop/controller/app/sorter.ts
@@ -0,0 +1,86 @@
+import {
+ CoolController,
+ BaseController,
+ CoolUrlTag,
+ TagTypes,
+ CoolTag,
+} from '@cool-midway/core';
+import { Body, Inject, Post } from '@midwayjs/core';
+import { ShopSorterService } from '../../service/sorter';
+
+/**
+ * 鍒嗘嫞鍛� App
+ */
+@CoolUrlTag()
+@CoolController()
+export class AppShopSorterController extends BaseController {
+ @Inject()
+ shopSorterService: ShopSorterService;
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/checkUser', { summary: '鎵弿鍥炴敹鐮佹牎楠屽钩鍙扮敤鎴�' })
+ async checkUser(@Body('unionid') unionid: string) {
+ return this.ok(await this.shopSorterService.checkUser(unionid));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/checkIn', { summary: '鎵撳崱绉垎' })
+ async checkIn(
+ @Body() body: { unionid: string; sorterId: string }
+ ) {
+ return this.ok(await this.shopSorterService.checkIn(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/itemList', { summary: '鍥炴敹鍝佺被鍒楄〃' })
+ async itemList(@Body('sorterId') sorterId: string) {
+ return this.ok(await this.shopSorterService.itemList(sorterId));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/saveOrder', { summary: '淇濆瓨鐜板満鎴栭绾﹀洖鏀舵竻鍗�' })
+ async saveOrder(
+ @Body()
+ body: {
+ sorterId: string;
+ unionid: string;
+ appointmentId?: number;
+ items: any[];
+ }
+ ) {
+ return this.ok(await this.shopSorterService.saveOrder(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/appointmentPage', { summary: '棰勭害鍥炴敹璁㈠崟鍒嗛〉' })
+ async appointmentPage(@Body() body) {
+ return this.ok(await this.shopSorterService.appointmentPage(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/confirmAppointment', { summary: '纭棰勭害鍗�' })
+ async confirmAppointment(
+ @Body() body: { id: number; sorterId: string }
+ ) {
+ return this.ok(await this.shopSorterService.confirmAppointment(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/claimAppointment', { summary: '棰嗗彇棰勭害鍗曞埌鑷繁鍚嶄笅' })
+ async claimAppointment(
+ @Body() body: { id: number; sorterId: string }
+ ) {
+ return this.ok(await this.shopSorterService.claimAppointment(body));
+ }
+
+ @CoolTag(TagTypes.IGNORE_TOKEN)
+ @Post('/scanAppointment', { summary: '鎵弿棰勭害鍗�' })
+ async scanAppointment(
+ @Body('appointmentNo') appointmentNo: string,
+ @Body('sorterId') sorterId: string
+ ) {
+ return this.ok(
+ await this.shopSorterService.scanAppointment(appointmentNo, sorterId)
+ );
+ }
+}
diff --git a/src/modules/shop/entity/recycleappointment.ts b/src/modules/shop/entity/recycleappointment.ts
index 62c5de5..a028ec0 100644
--- a/src/modules/shop/entity/recycleappointment.ts
+++ b/src/modules/shop/entity/recycleappointment.ts
@@ -50,7 +50,7 @@
@Column({ comment:"棰勮閲嶉噺",default: 0 })
expectedWeight:number;
- @Column({ comment: "鐘舵��", dict: ['寰呭鏍�', '寰呮帴鍗�', '宸叉帴鍗�', '涓婇棬涓�', '宸插畬鎴�','宸插彇娑�', '鏈煡'], default: '鏈煡' })
+ @Column({ comment: "鐘舵��", dict: ['鏂板', '寰呭鏍�', '寰呮帴鍗�', '宸茬‘璁�', '宸叉帴鍗�', '涓婇棬涓�', '宸插畬鎴�', '宸插彇娑�', '鏈煡'], default: '鏂板' })
status: string;
@Index()
diff --git a/src/modules/shop/entity/recycleappointmentlog.ts b/src/modules/shop/entity/recycleappointmentlog.ts
index 1c544a1..31a0bad 100644
--- a/src/modules/shop/entity/recycleappointmentlog.ts
+++ b/src/modules/shop/entity/recycleappointmentlog.ts
@@ -16,6 +16,7 @@
"鍒涘缓棰勭害",
"瀹℃牳閫氳繃",
"鍒嗛厤浜哄憳",
+ "纭",
"鎺ュ崟",
"鍒拌揪",
"寮�濮嬪洖鏀�",
diff --git a/src/modules/shop/entity/recycleorder.ts b/src/modules/shop/entity/recycleorder.ts
index 6abfb50..38bce50 100644
--- a/src/modules/shop/entity/recycleorder.ts
+++ b/src/modules/shop/entity/recycleorder.ts
@@ -10,7 +10,7 @@
@Column({ comment: "璁㈠崟缂栧彿" })
orderNo: string;
- @Column({ comment: "璁㈠崟绫诲瀷", dict: ['鐜板満', '棰勭害', '澶у睆', '娲诲姩'], default: '鐜板満' })
+ @Column({ comment: "璁㈠崟绫诲瀷", dict: ['鐜板満', '棰勭害', '澶у睆', '娲诲姩', '鎵撳崱'], default: '鐜板満' })
orderType: string;
@Index()
diff --git a/src/modules/shop/service/sorter.ts b/src/modules/shop/service/sorter.ts
new file mode 100644
index 0000000..1ecda8e
--- /dev/null
+++ b/src/modules/shop/service/sorter.ts
@@ -0,0 +1,531 @@
+import { Inject, Provide } from '@midwayjs/core';
+import { BaseService, CoolCommException, CoolTransaction } from '@cool-midway/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { QueryRunner, Repository } from 'typeorm';
+import * as moment from 'moment';
+import { UserInfoEntity } from '../../user/entity/info';
+import { BasicdataSorterEntity } from '../../basicdata/entity/sorter';
+import { RecycleOrderEntity } from '../entity/recycleorder';
+import { RecycleOrderItemEntity } from '../entity/recycleorderitem';
+import { RecycleItemEntity } from '../entity/recycleitem';
+import { RecycleTransactionEntity } from '../entity/transaction';
+import { RecycleAppointmentEntity } from '../entity/recycleappointment';
+import { RecycleAppointmentLogEntity } from '../entity/recycleappointmentlog';
+import { BaseSysParamService } from '../../base/service/sys/param';
+
+/**
+ * 鍒嗘嫞鍛� App 涓氬姟
+ */
+@Provide()
+export class ShopSorterService extends BaseService {
+ @InjectEntityModel(UserInfoEntity)
+ userInfoEntity: Repository<UserInfoEntity>;
+
+ @InjectEntityModel(BasicdataSorterEntity)
+ basicdataSorterEntity: Repository<BasicdataSorterEntity>;
+
+ @InjectEntityModel(RecycleOrderEntity)
+ recycleOrderEntity: Repository<RecycleOrderEntity>;
+
+ @InjectEntityModel(RecycleOrderItemEntity)
+ recycleOrderItemEntity: Repository<RecycleOrderItemEntity>;
+
+ @InjectEntityModel(RecycleItemEntity)
+ recycleItemEntity: Repository<RecycleItemEntity>;
+
+ @InjectEntityModel(RecycleTransactionEntity)
+ recycleTransactionEntity: Repository<RecycleTransactionEntity>;
+
+ @InjectEntityModel(RecycleAppointmentEntity)
+ recycleAppointmentEntity: Repository<RecycleAppointmentEntity>;
+
+ @InjectEntityModel(RecycleAppointmentLogEntity)
+ recycleAppointmentLogEntity: Repository<RecycleAppointmentLogEntity>;
+
+ @Inject()
+ baseSysParamService: BaseSysParamService;
+
+ /**
+ * 璇诲彇鎵撳崱鍙傛暟
+ */
+ async getHitupConfig() {
+ const scoreRaw = await this.baseSysParamService.dataByKey('hitup.score');
+ const maxRaw = await this.baseSysParamService.dataByKey(
+ 'hitup.maxmium.perday'
+ );
+ const score = Number(scoreRaw);
+ const maxPerDay = Number(maxRaw);
+ return {
+ score: Number.isFinite(score) && score >= 0 ? score : 1,
+ maxPerDay: Number.isFinite(maxPerDay) && maxPerDay >= 0 ? maxPerDay : 1,
+ };
+ }
+
+ /**
+ * 鏍¢獙鍒嗘嫞鍛�
+ */
+ async getSorter(sorterId: string) {
+ const id = String(sorterId || '').trim();
+ if (!id) {
+ throw new CoolCommException('缂哄皯鍒嗘嫞鍛樹俊鎭�');
+ }
+ const sorter = await this.basicdataSorterEntity.findOneBy({
+ businessId: id,
+ });
+ if (!sorter || sorter.status !== 0) {
+ throw new CoolCommException('鍒嗘嫞鍛樿处鍙蜂笉鍙敤');
+ }
+ return sorter;
+ }
+
+ /**
+ * 鏍¢獙鍥炴敹鐮佸搴旂殑骞冲彴鐢ㄦ埛锛堝洖鏀剁爜鍗充负 unionid锛�
+ */
+ async checkUser(unionid: string) {
+ const uid = String(unionid || '').trim();
+ if (!uid) {
+ throw new CoolCommException('璇锋壂鎻忔湁鏁堢殑鍥炴敹鐮�');
+ }
+ const user = await this.userInfoEntity.findOneBy({ unionid: uid });
+ if (!user || user.status !== 1) {
+ throw new CoolCommException('璇ョ敤鎴蜂笉鏄钩鍙扮敤鎴�');
+ }
+ return {
+ unionid: user.unionid,
+ nickName: user.nickName,
+ phone: user.phone,
+ role: user.role,
+ departmentId: user.departmentId,
+ carbonBalance: user.carbonBalance,
+ collegeName: user.collegeName,
+ className: user.className,
+ avatarUrl: user.avatarUrl,
+ };
+ }
+
+ /**
+ * 鎵撳崱绉垎
+ */
+ @CoolTransaction()
+ async checkIn(
+ param: { unionid: string; sorterId: string },
+ queryRunner?: QueryRunner
+ ) {
+ if (!queryRunner) {
+ throw new CoolCommException('浜嬪姟鍚姩澶辫触');
+ }
+ const manager = queryRunner.manager;
+ const unionid = String(param.unionid || '').trim();
+ const sorter = await this.getSorter(param.sorterId);
+ const user = await manager.findOne(UserInfoEntity, {
+ where: { unionid },
+ });
+ if (!user || user.status !== 1) {
+ throw new CoolCommException('璇ョ敤鎴蜂笉鏄钩鍙扮敤鎴�');
+ }
+
+ const { score, maxPerDay } = await this.getHitupConfig();
+ const today = moment().format('YYYY-MM-DD');
+ const todayCount = await manager
+ .createQueryBuilder(RecycleOrderEntity, 'a')
+ .where('a.campusUserId = :unionid', { unionid })
+ .andWhere('a.orderType = :orderType', { orderType: '鎵撳崱' })
+ .andWhere('a.createTime >= :start AND a.createTime <= :end', {
+ start: `${today} 00:00:00`,
+ end: `${today} 23:59:59`,
+ })
+ .getCount();
+
+ if (todayCount >= maxPerDay) {
+ throw new CoolCommException('璇ョ敤鎴蜂粖鏃ユ墦鍗℃鏁板凡杈句笂闄�');
+ }
+
+ const orderNo = this.genOrderNo('CI');
+ const now = moment().format('YYYY-MM-DD HH:mm:ss');
+ const order = await manager.save(RecycleOrderEntity, {
+ orderNo,
+ orderType: '鎵撳崱',
+ campusUserId: unionid,
+ departmentId: user.departmentId,
+ siteId: sorter.workSiteId,
+ sorterId: sorter.businessId,
+ status: '宸插畬鎴�',
+ totalWeight: 0,
+ totalAmount: 0,
+ totalCarbonPoint: score,
+ finishTime: now as any,
+ });
+
+ if (score > 0) {
+ await manager
+ .createQueryBuilder()
+ .update(UserInfoEntity)
+ .set({
+ carbonBalance: () => `carbonBalance + ${score}`,
+ })
+ .where('unionid = :unionid', { unionid })
+ .execute();
+
+ await manager.save(RecycleTransactionEntity, {
+ userId: unionid,
+ departmentId: user.departmentId,
+ type: '鏀跺叆',
+ amount: score,
+ sourceType: '绛惧埌',
+ remark: `鎵撳崱鑾峰緱纰崇Н鍒哷,
+ orderId: orderNo,
+ });
+ }
+
+ return {
+ orderNo,
+ orderId: order.id,
+ score,
+ nickName: user.nickName,
+ unionid,
+ };
+ }
+
+ /**
+ * 鍥炴敹鍝佺被鍒楄〃锛堝叏閮ㄥ惎鐢ㄥ搧绫伙紝鎸夌被鍒垎缁勫睍绀猴級
+ */
+ async itemList(_sorterId?: string) {
+ const list = await this.recycleItemEntity.find({
+ where: { status: 0 },
+ order: { id: 'ASC' },
+ });
+ if (list.length) {
+ return list;
+ }
+ return this.recycleItemEntity.find({
+ order: { id: 'ASC' },
+ });
+ }
+
+ /**
+ * 淇濆瓨鐜板満/棰勭害鍥炴敹娓呭崟
+ */
+ @CoolTransaction()
+ async saveOrder(
+ param: {
+ sorterId: string;
+ unionid: string;
+ appointmentId?: number;
+ items: any[];
+ },
+ queryRunner?: QueryRunner
+ ) {
+ if (!queryRunner) {
+ throw new CoolCommException('浜嬪姟鍚姩澶辫触');
+ }
+ const manager = queryRunner.manager;
+ const unionid = String(param.unionid || '').trim();
+ const sorter = await this.getSorter(param.sorterId);
+ const user = await manager.findOne(UserInfoEntity, {
+ where: { unionid },
+ });
+ if (!user || user.status !== 1) {
+ throw new CoolCommException('璇ョ敤鎴蜂笉鏄钩鍙扮敤鎴�');
+ }
+
+ const items = Array.isArray(param.items) ? param.items : [];
+ if (!items.length) {
+ throw new CoolCommException('璇锋坊鍔犲洖鏀舵竻鍗�');
+ }
+
+ let appointment: RecycleAppointmentEntity = null;
+ if (param.appointmentId) {
+ appointment = await manager.findOne(RecycleAppointmentEntity, {
+ where: { id: Number(param.appointmentId) },
+ });
+ if (!appointment) {
+ throw new CoolCommException('棰勭害鍗曚笉瀛樺湪');
+ }
+ if (appointment.status === '宸插畬鎴�') {
+ throw new CoolCommException('璇ラ绾﹀凡瀹屾垚');
+ }
+ if (appointment.status === '宸插彇娑�') {
+ throw new CoolCommException('璇ラ绾﹀凡鍙栨秷');
+ }
+ if (appointment.campusUserId && appointment.campusUserId !== unionid) {
+ throw new CoolCommException('鍥炴敹鐮佷笌棰勭害鍗曠敤鎴蜂笉涓�鑷�');
+ }
+ }
+
+ const itemRows = [];
+ let totalWeight = 0;
+ let totalAmount = 0;
+ let totalCarbonPoint = 0;
+
+ for (const row of items) {
+ const catalog = await manager.findOne(RecycleItemEntity, {
+ where: { itemId: String(row.itemId) },
+ });
+ if (!catalog) {
+ throw new CoolCommException(`鍥炴敹鍝佺被涓嶅瓨鍦細${row.itemId || ''}`);
+ }
+ const weight = Number(row.weight);
+ if (!weight || weight <= 0) {
+ throw new CoolCommException(`璇峰~鍐�${catalog.itemName}鐨勮閲廯);
+ }
+ const unitPrice = Number(catalog.price || 0);
+ const unitCarbon = Number(catalog.carbonPoint || 0);
+ const amount = Number((weight * unitPrice).toFixed(2));
+ const carbonPoint = Number((weight * unitCarbon).toFixed(2));
+ itemRows.push({
+ itemId: catalog.itemId,
+ itemName: catalog.itemName,
+ category: catalog.category,
+ weight,
+ unitPrice,
+ amount,
+ carbonPoint,
+ });
+ totalWeight += weight;
+ totalAmount += amount;
+ totalCarbonPoint += carbonPoint;
+ }
+
+ totalWeight = Number(totalWeight.toFixed(2));
+ totalAmount = Number(totalAmount.toFixed(2));
+ totalCarbonPoint = Number(totalCarbonPoint.toFixed(2));
+
+ const orderNo = this.genOrderNo(appointment ? 'YY' : 'XC');
+ const now = moment().format('YYYY-MM-DD HH:mm:ss');
+ const order = await manager.save(RecycleOrderEntity, {
+ orderNo,
+ orderType: appointment ? '棰勭害' : '鐜板満',
+ campusUserId: unionid,
+ departmentId: user.departmentId || sorter.departmentId,
+ siteId: sorter.workSiteId,
+ sorterId: sorter.businessId,
+ appointmentId: appointment ? appointment.id : null,
+ status: '宸插畬鎴�',
+ totalWeight,
+ totalAmount,
+ totalCarbonPoint,
+ finishTime: now as any,
+ });
+
+ await manager.save(
+ RecycleOrderItemEntity,
+ itemRows.map(item => ({
+ ...item,
+ orderNo,
+ }))
+ );
+
+ if (totalCarbonPoint > 0) {
+ await manager
+ .createQueryBuilder()
+ .update(UserInfoEntity)
+ .set({
+ carbonBalance: () => `carbonBalance + ${totalCarbonPoint}`,
+ })
+ .where('unionid = :unionid', { unionid })
+ .execute();
+
+ await manager.save(RecycleTransactionEntity, {
+ userId: unionid,
+ departmentId: user.departmentId,
+ type: '鏀跺叆',
+ amount: totalCarbonPoint,
+ sourceType: '鍥炴敹',
+ remark: `鍥炴敹璁㈠崟 ${orderNo} 鑾峰緱纰崇Н鍒哷,
+ orderId: orderNo,
+ });
+ }
+
+ if (appointment) {
+ await manager.update(RecycleAppointmentEntity, appointment.id, {
+ status: '宸插畬鎴�',
+ sorterId: sorter.businessId,
+ recycleOrderId: orderNo,
+ });
+ await manager.save(RecycleAppointmentLogEntity, {
+ appointmentId: appointment.id,
+ action: '瀹屾垚',
+ operatorType: '鍒嗘嫞鍛�',
+ operatorId: sorter.id,
+ remark: `涓婇棬鍥炴敹瀹屾垚锛岃鍗� ${orderNo}`,
+ });
+ }
+
+ return {
+ orderNo,
+ orderId: order.id,
+ totalWeight,
+ totalAmount,
+ totalCarbonPoint,
+ };
+ }
+
+ /**
+ * 棰勭害鍒楄〃
+ */
+ async appointmentPage(query: any) {
+ const { sorterId, status, page = 1, size = 10, keyWord, pool } = query || {};
+ const sorter = await this.getSorter(sorterId);
+ const find = this.recycleAppointmentEntity
+ .createQueryBuilder('a')
+ .select([
+ 'a.*',
+ 'b.nickName AS campusUserName',
+ 'c.businessName AS sorterName',
+ ])
+ .leftJoin(UserInfoEntity, 'b', 'a.campusUserId = b.unionid')
+ .leftJoin(BasicdataSorterEntity, 'c', 'a.sorterId = c.businessId');
+
+ if (sorter.departmentId) {
+ find.andWhere(
+ '(CAST(a.departmentId AS CHAR) = :deptId OR a.departmentId IS NULL)',
+ { deptId: String(sorter.departmentId) }
+ );
+ }
+ if (pool === 'new') {
+ find.andWhere('a.status IN (:...newSt)', {
+ newSt: ['鏂板', '寰呭鏍�', '寰呮帴鍗�', '鏈煡'],
+ });
+ find.andWhere(
+ "(a.sorterId IS NULL OR CAST(a.sorterId AS CHAR) = '' OR CAST(a.sorterId AS CHAR) = '0')"
+ );
+ } else if (pool === 'mine') {
+ find.andWhere('a.sorterId = :sid', { sid: sorter.businessId });
+ find.andWhere('a.status NOT IN (:...done)', {
+ done: ['宸插畬鎴�', '宸插彇娑�'],
+ });
+ } else if (status) {
+ find.andWhere('a.status = :status', { status });
+ }
+ if (keyWord) {
+ find.andWhere(
+ '(a.appointmentNo LIKE :kw OR a.contactName LIKE :kw OR a.contactPhone LIKE :kw OR a.address LIKE :kw)',
+ { kw: `%${keyWord}%` }
+ );
+ }
+
+ find.orderBy('a.appointmentDate', 'DESC');
+ find.addOrderBy('a.id', 'DESC');
+ return this.entityRenderPage(find, { ...query, page, size }, false);
+ }
+
+ /**
+ * 棰嗗彇棰勭害鍗曞埌鍒嗘嫞鍛樺悕涓�
+ */
+ async claimAppointment(param: { id: number; sorterId: string }) {
+ const sorter = await this.getSorter(param.sorterId);
+ const appointment = await this.recycleAppointmentEntity.findOneBy({
+ id: Number(param.id),
+ });
+ if (!appointment) {
+ throw new CoolCommException('棰勭害鍗曚笉瀛樺湪');
+ }
+ if (appointment.status === '宸插畬鎴�') {
+ throw new CoolCommException('璇ラ绾﹀凡瀹屾垚');
+ }
+ if (appointment.status === '宸插彇娑�') {
+ throw new CoolCommException('璇ラ绾﹀凡鍙栨秷');
+ }
+ if (
+ appointment.sorterId &&
+ String(appointment.sorterId) !== '0' &&
+ appointment.sorterId !== sorter.businessId
+ ) {
+ throw new CoolCommException('璇ラ绾﹀凡琚叾浠栧垎鎷e憳棰嗗彇');
+ }
+ await this.recycleAppointmentEntity.update(appointment.id, {
+ sorterId: sorter.businessId,
+ });
+ await this.recycleAppointmentLogEntity.save({
+ appointmentId: appointment.id,
+ action: '鎺ュ崟',
+ operatorType: '鍒嗘嫞鍛�',
+ operatorId: sorter.id,
+ remark: '鍒嗘嫞鍛橀鍙栭绾﹀崟',
+ });
+ return this.recycleAppointmentEntity.findOneBy({ id: appointment.id });
+ }
+
+ /**
+ * 纭棰勭害
+ */
+ @CoolTransaction()
+ async confirmAppointment(
+ param: { id: number; sorterId: string },
+ queryRunner?: QueryRunner
+ ) {
+ if (!queryRunner) {
+ throw new CoolCommException('浜嬪姟鍚姩澶辫触');
+ }
+ const manager = queryRunner.manager;
+ const sorter = await this.getSorter(param.sorterId);
+ const appointment = await manager.findOne(RecycleAppointmentEntity, {
+ where: { id: Number(param.id) },
+ });
+ if (!appointment) {
+ throw new CoolCommException('棰勭害鍗曚笉瀛樺湪');
+ }
+ if (appointment.status === '宸插畬鎴�') {
+ throw new CoolCommException('璇ラ绾﹀凡瀹屾垚');
+ }
+ if (appointment.status === '宸插彇娑�') {
+ throw new CoolCommException('璇ラ绾﹀凡鍙栨秷');
+ }
+ if (appointment.status !== '宸茬‘璁�') {
+ await manager.update(RecycleAppointmentEntity, appointment.id, {
+ status: '宸茬‘璁�',
+ sorterId: sorter.businessId,
+ });
+ await manager.save(RecycleAppointmentLogEntity, {
+ appointmentId: appointment.id,
+ action: '纭',
+ operatorType: '鍒嗘嫞鍛�',
+ operatorId: sorter.id,
+ remark: '鍒嗘嫞鍛樼‘璁ら绾�',
+ });
+ }
+ return await manager.findOne(RecycleAppointmentEntity, {
+ where: { id: appointment.id },
+ });
+ }
+
+ /**
+ * 鎵弿棰勭害鍗�
+ */
+ async scanAppointment(appointmentNo: string, sorterId: string) {
+ await this.getSorter(sorterId);
+ const no = String(appointmentNo || '').trim();
+ if (!no) {
+ throw new CoolCommException('璇锋壂鎻忔湁鏁堢殑棰勭害鍗�');
+ }
+ const appointment = await this.recycleAppointmentEntity.findOneBy({
+ appointmentNo: no,
+ }) || (/^\d+$/.test(no)
+ ? await this.recycleAppointmentEntity.findOneBy({ id: Number(no) })
+ : null);
+ if (!appointment) {
+ throw new CoolCommException('棰勭害鍗曚笉瀛樺湪');
+ }
+ if (appointment.status === '宸插彇娑�') {
+ throw new CoolCommException('璇ラ绾﹀凡鍙栨秷');
+ }
+ if (appointment.status === '宸插畬鎴�') {
+ throw new CoolCommException('璇ラ绾﹀凡瀹屾垚');
+ }
+ const user = appointment.campusUserId
+ ? await this.checkUser(appointment.campusUserId)
+ : null;
+ return {
+ ...appointment,
+ campusUser: user,
+ };
+ }
+
+ /**
+ * 鐢熸垚璁㈠崟缂栧彿
+ */
+ private genOrderNo(prefix: string) {
+ const rand = Math.floor(Math.random() * 90 + 10);
+ return `${prefix}${moment().format('YYYYMMDDHHmmssSSS')}${rand}`;
+ }
+}
diff --git a/src/modules/user/service/login.ts b/src/modules/user/service/login.ts
index 18d5cb3..283706d 100644
--- a/src/modules/user/service/login.ts
+++ b/src/modules/user/service/login.ts
@@ -287,18 +287,24 @@
const user = await this.basicdataSorterEntity.findOneBy({ phone });
if (user && user.password == md5(password)) {
-
+ if (user.status !== 0) {
+ throw new CoolCommException('鍒嗘嫞鍛樿处鍙蜂笉鍙敤');
+ }
const info = {
id: user.businessId,
- }
+ };
const { expire, refreshExpire } = this.jwtConfig;
return {
expire,
token: await this.generateSorterToken(info),
refreshExpire,
refreshToken: await this.generateSorterToken(info, true),
+ sorterId: user.businessId,
+ businessName: user.businessName,
+ phone: user.phone,
+ departmentId: user.departmentId,
+ workSiteId: user.workSiteId,
};
-
} else {
throw new CoolCommException('璐﹀彿鎴栧瘑鐮侀敊璇�');
}
@@ -346,7 +352,9 @@
*/
async generateSorterToken(info, isRefresh = false) {
const { expire, refreshExpire, secret } = this.jwtConfig;
- const user = await this.basicdataSorterEntity.findOneBy({ businessId: Equal(info.unionid) });
+ const user = await this.basicdataSorterEntity.findOneBy({
+ businessId: Equal(info.id),
+ });
const tokenInfo = {
isRefresh,
...info,
--
Gitblit v1.9.1