98ce961272c596468a4cb4b2498a3c70048e4aed..8e6c592cb1940a909d14a013de35767effbbad0a
2026-09-26 wangrong
修改首页显示bug
8e6c59 对比 | 目录
2026-09-26 wangrong
修改首页显示和统计任务
22c5f3 对比 | 目录
5个文件已修改
155 ■■■■■ 已修改文件
src/config/config.local.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config/config.prod.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/modules/base/middleware/authority.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/modules/report/service/screen.ts 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/modules/report/service/taskservicesite.ts 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config/config.local.ts
@@ -15,10 +15,10 @@
      default: {
        type: 'mysql',
        host: '127.0.0.1',
        port: 8806,
        port: 3306,
        username: 'womp',
        password: 'Sening@2023',
        database: 'womp-shop',
        database: 'womp_shop',
        // 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
        synchronize: false,
        // 打印日志
src/config/config.prod.ts
@@ -27,10 +27,10 @@
      default: {
        type: 'mysql',
        host: '127.0.0.1',
        port: 8806,
        port: 3306,
        username: 'womp',
        password: 'Sening@2023',
        database: 'womp-shop',
        database: 'womp_shop',
        // 自动建表 注意:线上部署的时候不要使用,有可能导致数据丢失
        synchronize: false,
        // 打印日志
src/modules/base/middleware/authority.ts
@@ -100,7 +100,9 @@
          if (
            new RegExp(`^${adminUrl}?.*/comm/`).test(url) ||
            // 字典接口
            url == '/admin/dict/info/data'
            url == '/admin/dict/info/data' ||
            // 驾驶舱首页汇总:菜单「首页」没有按钮权限,未登记 report:screen:query 时非超管会 403
            url == '/admin/report/screen/query'
          ) {
            await next();
            return;
src/modules/report/service/screen.ts
@@ -28,7 +28,7 @@
const ITEM_COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#a855f7', '#06b6d4', '#ef4444', '#94a3b8'];
/** 日表列与垃圾种类编码。首页分类统计只累加其中状态为启用的编码。 */
/** 日表列与垃圾种类编码,首页 KPI/趋势口径以本表为准,不读种类表启用状态。 */
const WASTE_FIELDS: {
  code: string;
  weight: string;
@@ -51,6 +51,9 @@
    group: (idx === '006' ? 'elec' : idx === '004' ? 'fabric' : undefined) as 'elec' | 'fabric' | undefined,
  })),
];
/** 五大类;子类已滚入 SW62,总量不再把 SW62001–007 加一遍。 */
const MAIN_WASTE_FIELDS = WASTE_FIELDS.filter(f => /^SW6[0-4]$/.test(f.code));
type OrdersByType = {
  axis: string[];
@@ -143,21 +146,21 @@
      safe('年度站点日汇总', () => this.sumSite(yearStart, yearEnd), emptySum),
    ]);
    const enabledWaste = await safe(
      '启用垃圾种类',
      () => this.enabledWaste(),
      { names: new Map<string, string>(), codes: new Set<string>() }
    const wasteNames = await safe(
      '垃圾种类名称',
      () => this.wasteNames(),
      new Map<string, string>()
    );
    const wasteMix = this.buildWasteMix(period, enabledWaste);
    const recycleItems = this.buildRecycleItems(period, enabledWaste);
    const wasteMix = this.buildWasteMix(period, wasteNames);
    const recycleItems = this.buildRecycleItems(period, wasteNames);
    const { axis, trend } = await safe(
      '回收趋势',
      () => this.buildTrend(range || 'month', dates, enabledWaste.codes),
      () => this.buildTrend(range || 'month', dates),
      { axis: [], trend: { weight: [], value: [], carbon: [] } }
    );
    const yearCarbon = await safe(
      '年度碳减排',
      () => this.buildYearCarbon(enabledWaste.codes),
      () => this.buildYearCarbon(),
      Array.from({ length: 12 }, () => 0)
    );
    const ranks = await safe(
@@ -208,10 +211,10 @@
      sites: [],
    });
    const periodStat = this.statEnabled(period, enabledWaste.codes);
    const todayStat = this.statEnabled(todayRow, enabledWaste.codes);
    const totalStat = this.statEnabled(totalRow, enabledWaste.codes);
    const yearStat = this.statEnabled(yearRow, enabledWaste.codes);
    const periodStat = this.statKpi(period);
    const todayStat = this.statKpi(todayRow);
    const totalStat = this.statKpi(totalRow);
    const yearStat = this.statKpi(yearRow);
    return {
      range: range || 'month',
@@ -357,6 +360,11 @@
      .addSelect('SUM(a.timeSw63)', 'timeSw63')
      .addSelect('SUM(a.timeSw64)', 'timeSw64')
      .addSelect('SUM(a.carbonTotal)', 'carbonTotal')
      .addSelect('SUM(a.carbonSw60)', 'carbonSw60')
      .addSelect('SUM(a.carbonSw61)', 'carbonSw61')
      .addSelect('SUM(a.carbonSw62)', 'carbonSw62')
      .addSelect('SUM(a.carbonSw63)', 'carbonSw63')
      .addSelect('SUM(a.carbonSw64)', 'carbonSw64')
      .addSelect('SUM(a.estimatedSalesSw62)', 'estimatedSalesSw62');
    for (const i of ['001', '002', '003', '004', '005', '006', '007']) {
@@ -371,32 +379,30 @@
    return (await qb.getRawOne()) || {};
  }
  private async enabledWaste() {
  /** 种类名称仅作展示,不参与 KPI 过滤。 */
  private async wasteNames() {
    const list = await this.wasteTypeRepo.find({
      where: { status: 1 },
      select: ['code', 'name'],
    });
    const names = new Map<string, string>();
    const codes = new Set<string>();
    list.forEach(e => {
      const code = String(e.code).toUpperCase();
      codes.add(code);
      names.set(code, e.name);
      names.set(String(e.code).toUpperCase(), e.name);
    });
    return { names, codes };
    return names;
  }
  /** 只累加启用种类对应的日表列,避免禁用种类进入重量、碳排和销售额。 */
  private statEnabled(row: any, codes: Set<string>) {
    const on = WASTE_FIELDS.filter(f => codes.has(f.code));
    const sum = (pick: (f: (typeof WASTE_FIELDS)[number]) => string | undefined) =>
      on.reduce((s, f) => s + n(row?.[pick(f)]), 0);
  /** 按 WASTE_FIELDS 汇总;总量用五大类,分组指标用 group 字段。 */
  private statKpi(row: any) {
    const sum = (
      fields: typeof WASTE_FIELDS,
      pick: (f: (typeof WASTE_FIELDS)[number]) => string | undefined
    ) => fields.reduce((s, f) => s + n(row?.[pick(f)]), 0);
    const ofGroup = (group: 'recyclable' | 'bulk' | 'elec' | 'fabric') =>
      on.filter(f => f.group === group).reduce((s, f) => s + n(row?.[f.weight]), 0);
      WASTE_FIELDS.filter(f => f.group === group).reduce((s, f) => s + n(row?.[f.weight]), 0);
    return {
      weight: sum(f => f.weight),
      carbon: sum(f => f.carbon),
      sales: sum(f => f.sales),
      weight: sum(MAIN_WASTE_FIELDS, f => f.weight),
      carbon: sum(MAIN_WASTE_FIELDS, f => f.carbon),
      sales: sum(MAIN_WASTE_FIELDS, f => f.sales),
      recyclable: ofGroup('recyclable'),
      bulk: ofGroup('bulk'),
      elec: ofGroup('elec'),
@@ -404,24 +410,23 @@
    };
  }
  private enabledExpr(codes: Set<string>, pick: (f: (typeof WASTE_FIELDS)[number]) => string | undefined) {
    const cols = WASTE_FIELDS.filter(f => codes.has(f.code))
      .map(pick)
      .filter((col): col is string => !!col);
  private kpiExpr(pick: (f: (typeof WASTE_FIELDS)[number]) => string | undefined) {
    const cols = MAIN_WASTE_FIELDS.map(pick).filter((col): col is string => !!col);
    if (!cols.length) return '0';
    return cols.map(col => `IFNULL(a.${col},0)`).join(' + ');
  }
  private buildWasteMix(row: any, waste: { names: Map<string, string>; codes: Set<string> }) {
  /** 首页分类全量返回,不按种类启用状态裁剪;0 值由前端图表隐藏。 */
  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' },
    ].filter(e => waste.codes.has(e.code));
    ];
    return items.map(e => ({
      name: waste.names.get(e.code) || this.defaultWasteName(e.code),
      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,
@@ -442,13 +447,13 @@
    );
  }
  private buildRecycleItems(row: any, waste: { names: Map<string, string>; codes: Set<string> }) {
  /** 可回收 7 子类全量返回;碳值保留符号(减排可为负),图表侧取绝对值展示。 */
  private buildRecycleItems(row: any, names: Map<string, string>) {
    const fallback = ['废纸', '塑料', '金属', '织物', '玻璃', '电子产品', '其他可回收'];
    return ['001', '002', '003', '004', '005', '006', '007']
      .map((idx, i) => ({ idx, i, code: `SW62${idx}` }))
      .filter(e => waste.codes.has(e.code))
      .map(e => ({
        name: waste.names.get(e.code) || fallback[e.i],
        name: names.get(e.code) || fallback[e.i],
        code: e.code,
        weight: round(n(row[`weightSw62${e.idx}`]), 1),
        value: round(n(row[`estimatedSalesSw62${e.idx}`]), 0),
@@ -459,8 +464,7 @@
  private async buildTrend(
    range: string,
    dates: { start: string; end: string; startTime: string; endTime: string },
    codes: Set<string>
    dates: { start: string; end: string; startTime: string; endTime: string }
  ) {
    const trendStart =
      range === 'today' ? moment().subtract(6, 'day').format('YYYY-MM-DD') : dates.start;
@@ -469,9 +473,9 @@
    const qb = this.dailySite
      .createQueryBuilder('a')
      .select('a.date', 'date')
      .addSelect(`SUM(${this.enabledExpr(codes, f => f.weight)})`, 'weight')
      .addSelect(`SUM(${this.enabledExpr(codes, f => f.sales)})`, 'value')
      .addSelect(`SUM(${this.enabledExpr(codes, f => f.carbon)})`, 'carbon')
      .addSelect(`SUM(${this.kpiExpr(f => f.weight)})`, 'weight')
      .addSelect(`SUM(${this.kpiExpr(f => f.sales)})`, 'value')
      .addSelect(`SUM(${this.kpiExpr(f => f.carbon)})`, 'carbon')
      .where('a.date BETWEEN :start AND :end', { start: trendStart, end: trendEnd })
      .groupBy('a.date')
      .orderBy('a.date', 'ASC');
@@ -524,13 +528,13 @@
    return { axis, trend: { weight, value, carbon } };
  }
  private async buildYearCarbon(codes: Set<string>) {
  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(${this.enabledExpr(codes, f => f.carbon)})`, 'carbon')
      .addSelect(`SUM(${this.kpiExpr(f => f.carbon)})`, 'carbon')
      .where('a.date BETWEEN :start AND :end', { start, end })
      .groupBy('MONTH(a.date)');
    this.applyDept(qb, 'a');
src/modules/report/service/taskservicesite.ts
@@ -55,18 +55,33 @@
        // 3. 内存聚合核心:定义支持动态键名累加的对象字典
        const siteAggMap = new Map<string, any>();
        // 映射表:将明细里的 garbageType 映射到你 Entity 里的字段后缀
        // 源数据 garbageType 为大写 SW62001…SW62007,没有单独的 SW62 流水。
        // 先按 7 个子类分别累计,再把重量/碳排/销售额汇总进 SW62。
        const SW62_SUB_CODES = [
            'SW62001', 'SW62002', 'SW62003', 'SW62004',
            'SW62005', 'SW62006', 'SW62007',
        ];
        const SW62_SUB_SUFFIXES = SW62_SUB_CODES.map(code => `Sw${code.slice(2)}`);
        const typeSuffixes = [
            'Sw60', 'Sw61', 'Sw62', 'Sw63', 'Sw64',
            ...SW62_SUB_SUFFIXES,
        ];
        const typeKeyMap: Record<string, string> = {
            'SW60': 'Sw60', 'SW61': 'Sw61', 'SW62': 'Sw62', 'SW63': 'Sw63', 'SW64': 'Sw64',
            'Sw62001': 'Sw62001', 'Sw62002': 'Sw62002', 'Sw62003': 'Sw62003',
            'Sw62004': 'Sw62004', 'Sw62005': 'Sw62005', 'Sw62006': 'Sw62006', 'Sw62007': 'Sw62007'
            SW60: 'Sw60',
            SW61: 'Sw61',
            SW63: 'Sw63',
            SW64: 'Sw64',
        };
        for (const code of SW62_SUB_CODES) {
            const suffix = `Sw${code.slice(2)}`;
            typeKeyMap[code] = suffix;
            typeKeyMap[suffix] = suffix;
        }
        for (const row of rawDetails) {
            const siteId = row.siteId;
            const weight = Number(row.weight) || 0;
            const gType = row.garbageType || 'SW64';
            const gType = String(row.garbageType || 'SW64').trim().toUpperCase();
            const fieldSuffix = typeKeyMap[gType];
            // 初始化该站点的聚合结构
@@ -79,7 +94,7 @@
                    weightTotal: 0, timeTotal: 0, carbonTotal: 0
                };
                // 动态初始化宽表里各子类型的 0 值
                Object.values(typeKeyMap).forEach(suffix => {
                typeSuffixes.forEach(suffix => {
                    initData[`weight${suffix}`] = 0;
                    initData[`time${suffix}`] = 0;
                    initData[`carbon${suffix}`] = 0;
@@ -119,6 +134,24 @@
            }
        }
        // SW62 没有独立流水,用 7 个子类重量/碳排/销售额加总写入父类字段
        for (const agg of siteAggMap.values()) {
            let weightSw62 = 0;
            let carbonSw62 = 0;
            let salesSw62 = 0;
            let timeSw62 = 0;
            for (const suffix of SW62_SUB_SUFFIXES) {
                weightSw62 += Number(agg[`weight${suffix}`]) || 0;
                carbonSw62 += Number(agg[`carbon${suffix}`]) || 0;
                salesSw62 += Number(agg[`estimatedSales${suffix}`]) || 0;
                timeSw62 = Math.max(timeSw62, Number(agg[`time${suffix}`]) || 0);
            }
            agg.weightSw62 = weightSw62;
            agg.carbonSw62 = carbonSw62;
            agg.estimatedSalesSw62 = salesSw62;
            agg.timeSw62 = timeSw62;
        }
        // 4. 高性能写入:转换为数组,利用批量插入 + ON DUPLICATE KEY UPDATE 提交给 MySQL
        const finalRecords = Array.from(siteAggMap.values()).map(item => {
            // 浮点数防精度丢失处理,保留两位小数