| | |
| | | entity: BasicdataWastetypeEntity, |
| | | service: BasicdataWastetypeService, |
| | | listQueryOp: { |
| | | fieldEq: ['a.status', 'a.isRecyclable'], |
| | | addOrderBy: { |
| | | 'a.code': 'ASC', |
| | | }, |
| | |
| | | keyWordLikeFields: ['a.sn'], |
| | | fieldEq: ['a.status'], |
| | | where: async (ctx) => { |
| | | const { createDate, gpsTime } = ctx.request.body; |
| | | const { createDate, gpsTime, startTime, endTime } = ctx.request.body; |
| | | const condition:any[][] = []; |
| | | if (startTime && endTime) { |
| | | condition.push(['a.createDate BETWEEN :startTime AND :endTime', { startTime, endTime }]); |
| | | } |
| | | if (createDate && createDate.length === 2) { |
| | | condition.push(['a.createDate BETWEEN :createStartDate AND :createEndDate', { createStartDate: createDate[0], createEndDate: createDate[1] }]); |
| | | } |
| | |
| | | ], |
| | | fieldEq: [ |
| | | { column: 'a.devSerial', requestParam: 'devSerial' }, |
| | | { column: 'a.alarmType', requestParam: 'alarmType' }, |
| | | { column: 'c.name', requestParam: 'departmentName' } |
| | | { column: 'a.alarmType', requestParam: 'alarmType' } |
| | | ], |
| | | fieldLike: [ |
| | | { column: 'c.name', requestParam: 'departmentName' } |
| | | ] |
| | | ], |
| | | where: async ctx => { |
| | | const { startTime, endTime } = ctx.request.body; |
| | | const conditions: any[][] = []; |
| | | if (startTime && endTime) { |
| | | conditions.push(['a.alarmTime BETWEEN :startTime AND :endTime', { startTime, endTime }]); |
| | | } |
| | | return conditions; |
| | | } |
| | | } |
| | | }) |
| | | export class AdminPushDevicealarmController extends BaseController {} |
| | |
| | | const condition = []; |
| | | if (startTime && endTime) { |
| | | condition.push([ |
| | | 'a.occurTime BETWEEN :startTime AND :endTime', |
| | | 'a.messageTime BETWEEN :startTime AND :endTime', |
| | | { startTime, endTime } |
| | | ]); |
| | | } |
| | |
| | | 'd.businessName AS businessName', 'd.siteType AS siteType', // 垃圾名称 |
| | | |
| | | ], |
| | | fieldEq: ['a.deviceNo','d.siteType'], |
| | | fieldEq: ['d.siteType'], |
| | | fieldLike: ['a.deviceNo'], |
| | | join: [ |
| | | { |
| | |
| | | }, |
| | | ], |
| | | where: async ctx => { |
| | | const { startTime, endTime } = ctx.request.body; |
| | | const conditions: any[][] = []; |
| | | if (startTime && endTime) { |
| | | conditions.push(['a.uploadTime BETWEEN :startTime AND :endTime', { startTime, endTime }]); |
| | | } |
| | | conditions.push(...(await WithDeptFilterWhere(ctx, { alias: 'a', field: 'departmentId' }))); |
| | | |
| | | // 其他关联表(举例) |
| | |
| | | }, |
| | | ], |
| | | where: async (ctx) => { |
| | | const { createDate, gpsTime } = ctx.request.body; |
| | | const { createDate, gpsTime, startTime, endTime } = ctx.request.body; |
| | | const conditions: any[][] = []; |
| | | if (startTime && endTime) { |
| | | conditions.push(['a.gpsTime BETWEEN :startTime AND :endTime', { startTime, endTime }]); |
| | | } |
| | | if (createDate && createDate.length === 2) { |
| | | conditions.push(['a.createDate BETWEEN :createStartDate AND :createEndDate', { createStartDate: createDate[0], createEndDate: createDate[1] }]); |
| | | } |
| | |
| | | import { CoolController, BaseController } from '@cool-midway/core'; |
| | | import { PushWeightEntity } from '../../entity/weight'; |
| | | import { PushWeightService } from '../../service/weight'; |
| | | import { BasicdataWastetypeEntity } from '../../../basicdata/entity/wastetype'; |
| | | |
| | | /** |
| | | * 原始称重数据管理 |
| | |
| | | entity: PushWeightEntity, |
| | | service: PushWeightService, |
| | | pageQueryOp: { |
| | | select: ['a.*'], |
| | | select: ['a.*', 'b.name AS garbageName'], |
| | | fieldEq: ['a.deviceNo', 'a.garbageType'], |
| | | join: [ |
| | | { |
| | | entity: BasicdataWastetypeEntity, |
| | | alias: 'b', |
| | | condition: 'a.garbageType = b.code', |
| | | type: 'leftJoin', |
| | | }, |
| | | ], |
| | | where: async ctx => { |
| | | const { startTime, endTime } = ctx.request.body; |
| | | const conditions = []; |
| | |
| | | keyWordLikeFields: ['a.departmentName'], |
| | | fieldEq: ['a.date', 'a.departmentId'], |
| | | where: async (ctx) => { |
| | | const { startTime, endTime } = ctx.request.body; |
| | | const conditions: any[][] = []; |
| | | if (startTime && endTime) { |
| | | conditions.push(['a.date BETWEEN :startTime AND :endTime', { startTime, endTime }]); |
| | | } |
| | | conditions.push(...(await WithDeptFilterWhere(ctx, { alias: 'a', field: 'departmentId' }))); |
| | | return conditions; |
| | | } |
| | |
| | | ], |
| | | |
| | | where: async (ctx) => { |
| | | const { startTime, endTime } = ctx.request.body; |
| | | const conditions: any[][] = []; |
| | | if (startTime && endTime) { |
| | | conditions.push(['a.date BETWEEN :startTime AND :endTime', { startTime, endTime }]); |
| | | } |
| | | conditions.push(...(await WithDeptFilterWhere(ctx, { alias: 'a', field: 'departmentId' }))); |
| | | return conditions; |
| | | } |
| | |
| | | ], |
| | | |
| | | where: async (ctx) => { |
| | | const { startTime, endTime } = ctx.request.body; |
| | | const conditions: any[][] = []; |
| | | if (startTime && endTime) { |
| | | conditions.push(['a.date BETWEEN :startTime AND :endTime', { startTime, endTime }]); |
| | | } |
| | | conditions.push(...(await WithDeptFilterWhere(ctx, { alias: 'a', field: 'departmentId' }))); |
| | | return conditions; |
| | | } |
| | |
| | | service: ReportJoblogService, |
| | | pageQueryOp: { |
| | | keyWordLikeFields: ['a.jobName'], |
| | | fieldEq: ['a.dataDate', 'a.status'] |
| | | fieldEq: ['a.dataDate', 'a.status'], |
| | | where: async ctx => { |
| | | const { startTime, endTime } = ctx.request.body; |
| | | const conditions: any[][] = []; |
| | | if (startTime && endTime) { |
| | | conditions.push(['a.dataDate BETWEEN :startTime AND :endTime', { startTime, endTime }]); |
| | | } |
| | | return conditions; |
| | | } |
| | | } |
| | | }) |
| | | export class AdminReportJoblogController extends BaseController {} |
| | |
| | | |
| | | const ITEM_COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#a855f7', '#06b6d4', '#ef4444', '#94a3b8']; |
| | | |
| | | /** 日表列与垃圾种类编码。首页分类统计只累加其中状态为启用的编码。 */ |
| | | const WASTE_FIELDS: { |
| | | code: string; |
| | | weight: string; |
| | | carbon: string; |
| | | time: string; |
| | | sales?: string; |
| | | group?: 'recyclable' | 'bulk' | 'elec' | 'fabric'; |
| | | }[] = [ |
| | | { code: 'SW60', weight: 'weightSw60', carbon: 'carbonSw60', time: 'timeSw60' }, |
| | | { code: 'SW61', weight: 'weightSw61', carbon: 'carbonSw61', time: 'timeSw61' }, |
| | | { code: 'SW62', weight: 'weightSw62', carbon: 'carbonSw62', time: 'timeSw62', sales: 'estimatedSalesSw62', group: 'recyclable' }, |
| | | { code: 'SW63', weight: 'weightSw63', carbon: 'carbonSw63', time: 'timeSw63', group: 'bulk' }, |
| | | { code: 'SW64', weight: 'weightSw64', carbon: 'carbonSw64', time: 'timeSw64' }, |
| | | ...['001', '002', '003', '004', '005', '006', '007'].map(idx => ({ |
| | | code: `SW62${idx}`, |
| | | weight: `weightSw62${idx}`, |
| | | carbon: `carbonSw62${idx}`, |
| | | time: `timeSw62${idx}`, |
| | | sales: `estimatedSalesSw62${idx}`, |
| | | group: (idx === '006' ? 'elec' : idx === '004' ? 'fabric' : undefined) as 'elec' | 'fabric' | undefined, |
| | | })), |
| | | ]; |
| | | |
| | | type OrdersByType = { |
| | | axis: string[]; |
| | | '现场回收': number[]; |
| | |
| | | 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 enabledWaste = await safe( |
| | | '启用垃圾种类', |
| | | () => this.enabledWaste(), |
| | | { names: new Map<string, string>(), codes: new Set<string>() } |
| | | ); |
| | | const wasteMix = this.buildWasteMix(period, enabledWaste); |
| | | const recycleItems = this.buildRecycleItems(period, enabledWaste); |
| | | const { axis, trend } = await safe( |
| | | '回收趋势', |
| | | () => this.buildTrend(range || 'month', dates), |
| | | () => this.buildTrend(range || 'month', dates, enabledWaste.codes), |
| | | { axis: [], trend: { weight: [], value: [], carbon: [] } } |
| | | ); |
| | | const yearCarbon = await safe('年度碳减排', () => this.buildYearCarbon(), Array.from({ length: 12 }, () => 0)); |
| | | const yearCarbon = await safe( |
| | | '年度碳减排', |
| | | () => this.buildYearCarbon(enabledWaste.codes), |
| | | Array.from({ length: 12 }, () => 0) |
| | | ); |
| | | const ranks = await safe( |
| | | '班级/专业/院系排行', |
| | | () => this.buildOrgRanks(dates.startTime, dates.endTime, unavailable), |
| | |
| | | sites: [], |
| | | }); |
| | | |
| | | const weightKg = n(period.weightTotal); |
| | | const recyclableKg = n(period.weightSw62); |
| | | const carbonKg = n(period.carbonTotal); |
| | | const totalCarbonKg = n(totalRow.carbonTotal); |
| | | 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); |
| | | |
| | | 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), |
| | | todayWeightKg: round(todayStat.weight, 1), |
| | | periodWeightTon: round(periodStat.weight / 1000, 2), |
| | | resourceRate: periodStat.weight > 0 ? round((periodStat.recyclable / periodStat.weight) * 100, 1) : 0, |
| | | periodValue: round(periodStat.sales, 0), |
| | | periodCarbonTon: round(periodStat.carbon / 1000, 2), |
| | | totalWeightTon: round(totalStat.weight / 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), |
| | | recyclableTon: round(periodStat.recyclable / 1000, 2), |
| | | bulkTon: round(periodStat.bulk / 1000, 2), |
| | | elecTon: round(periodStat.elec / 1000, 2), |
| | | fabricTon: round(periodStat.fabric / 1000, 2), |
| | | yearCarbonTon: round(yearStat.carbon / 1000, 1), |
| | | monthCarbonTon: round(periodStat.carbon / 1000, 2), |
| | | totalCarbonTon: round(totalStat.carbon / 1000, 1), |
| | | greenActions: users.greenActions, |
| | | newParticipants: users.newParticipants, |
| | | participateRate: users.participateRate, |
| | |
| | | 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 async enabledWaste() { |
| | | 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); |
| | | }); |
| | | return { names, codes }; |
| | | } |
| | | |
| | | private buildWasteMix(row: any, names: Map<string, string>) { |
| | | /** 只累加启用种类对应的日表列,避免禁用种类进入重量、碳排和销售额。 */ |
| | | 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); |
| | | const ofGroup = (group: 'recyclable' | 'bulk' | 'elec' | 'fabric') => |
| | | on.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), |
| | | recyclable: ofGroup('recyclable'), |
| | | bulk: ofGroup('bulk'), |
| | | elec: ofGroup('elec'), |
| | | fabric: ofGroup('fabric'), |
| | | }; |
| | | } |
| | | |
| | | 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); |
| | | 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> }) { |
| | | 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: names.get(e.code) || this.defaultWasteName(e.code), |
| | | name: waste.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, |
| | |
| | | ); |
| | | } |
| | | |
| | | 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', |
| | | ]; |
| | | private buildRecycleItems(row: any, waste: { names: Map<string, string>; codes: Set<string> }) { |
| | | 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], |
| | | }; |
| | | }); |
| | | 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], |
| | | code: e.code, |
| | | weight: round(n(row[`weightSw62${e.idx}`]), 1), |
| | | value: round(n(row[`estimatedSalesSw62${e.idx}`]), 0), |
| | | carbon: round(n(row[`carbonSw62${e.idx}`]), 1), |
| | | color: ITEM_COLORS[e.i], |
| | | })); |
| | | } |
| | | |
| | | private async buildTrend(range: string, dates: { start: string; end: string; startTime: string; endTime: string }) { |
| | | private async buildTrend( |
| | | range: string, |
| | | dates: { start: string; end: string; startTime: string; endTime: string }, |
| | | codes: Set<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') |
| | | .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') |
| | | .where('a.date BETWEEN :start AND :end', { start: trendStart, end: trendEnd }) |
| | | .groupBy('a.date') |
| | | .orderBy('a.date', 'ASC'); |
| | |
| | | return { axis, trend: { weight, value, carbon } }; |
| | | } |
| | | |
| | | private async buildYearCarbon() { |
| | | private async buildYearCarbon(codes: Set<string>) { |
| | | 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') |
| | | .addSelect(`SUM(${this.enabledExpr(codes, f => f.carbon)})`, 'carbon') |
| | | .where('a.date BETWEEN :start AND :end', { start, end }) |
| | | .groupBy('MONTH(a.date)'); |
| | | this.applyDept(qb, 'a'); |
| | |
| | | /** |
| | | * App 端分页:转赠 tab 只看赠送/接受,收支 tab 看其余类型 |
| | | */ |
| | | async page(query: any) { |
| | | async page(query: any, option?: any, connectionName?: any) { |
| | | // 管理端列表不带 tab,走通用分页,检索条件由 pageQueryOp 生效 |
| | | if (!query?.tab) { |
| | | return super.page(query, option, connectionName); |
| | | } |
| | | |
| | | const { userId, tab, page = 1, size = 10 } = query || {}; |
| | | const find = this.recycleTransactionEntity.createQueryBuilder('a'); |
| | | |
| | |
| | | |
| | | find.orderBy('a.createTime', 'DESC'); |
| | | find.addOrderBy('a.id', 'DESC'); |
| | | |
| | | console.log(find) |
| | | |
| | | return this.entityRenderPage(find, { ...query, page, size }, false); |
| | | } |