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