133 lines
4.7 KiB
Plaintext
133 lines
4.7 KiB
Plaintext
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
|
||
import { computeDateRange } from './dateRange.uts'
|
||
|
||
export type DeliveryAnalysisData = {
|
||
trendList: Array<UTSJSONObject>
|
||
topList: Array<UTSJSONObject>
|
||
startIso: string
|
||
endIso: string
|
||
}
|
||
|
||
export async function fetchDeliveryAnalysis(period: string): Promise<DeliveryAnalysisData> {
|
||
const { startIso, endIso } = computeDateRange(period)
|
||
|
||
await ensureSupabaseReady()
|
||
|
||
// 优先走 RPC(需要在 Supabase 执行 DELIVERY_ANALYSIS_RPCS.sql 创建函数)
|
||
let trendList: Array<UTSJSONObject> = []
|
||
let topList: Array<UTSJSONObject> = []
|
||
|
||
const trendRes: any = await supa.rpc('rpc_delivery_efficiency_daily', {
|
||
p_start: startIso,
|
||
p_end: endIso
|
||
} as UTSJSONObject)
|
||
|
||
if (trendRes.status === 404) {
|
||
// RPC 不存在:降级到直查表聚合(测试阶段兜底)
|
||
const taskRes: any = await supa
|
||
.from('ml_delivery_tasks')
|
||
.select('id,driver_id,assigned_at,delivered_at,delivery_fee', {})
|
||
.eq('status', 5)
|
||
.gte('assigned_at', startIso)
|
||
.order('assigned_at', { ascending: true } as any)
|
||
.execute()
|
||
|
||
if (taskRes?.error != null) throw taskRes.error
|
||
|
||
const rowsAny = (taskRes.data != null ? taskRes.data : []) as any
|
||
const tasks = Array.isArray(rowsAny) ? (rowsAny as Array<UTSJSONObject>) : []
|
||
|
||
const dayAgg = new Map<string, UTSJSONObject>()
|
||
const driverAgg = new Map<string, number>()
|
||
const driverFeeAgg = new Map<string, number>()
|
||
const driverTimeAgg = new Map<string, number>()
|
||
|
||
for (let i = 0; i < tasks.length; i++) {
|
||
const t = tasks[i]
|
||
const assignedAt = t.getString('assigned_at') ?? ''
|
||
const deliveredAt = t.getString('delivered_at') ?? ''
|
||
const driverId = t.getString('driver_id') ?? ''
|
||
if (assignedAt.trim() === '' || deliveredAt.trim() === '') continue
|
||
|
||
const day = assignedAt.length >= 10 ? assignedAt.substring(0, 10) : assignedAt
|
||
const a = new Date(assignedAt)
|
||
const d = new Date(deliveredAt)
|
||
const diffMin = Math.max(0, (d.getTime() - a.getTime()) / 60000)
|
||
const fee = t.getNumber('delivery_fee') ?? 0
|
||
|
||
const old = dayAgg.get(day)
|
||
if (old == null) {
|
||
const obj = new UTSJSONObject()
|
||
obj.set('day', day)
|
||
obj.set('completed_orders', 1)
|
||
obj.set('sum_minutes', diffMin)
|
||
obj.set('total_fee', fee)
|
||
dayAgg.set(day, obj)
|
||
} else {
|
||
old.set('completed_orders', (old.getNumber('completed_orders') ?? 0) + 1)
|
||
old.set('sum_minutes', (old.getNumber('sum_minutes') ?? 0) + diffMin)
|
||
old.set('total_fee', (old.getNumber('total_fee') ?? 0) + fee)
|
||
}
|
||
|
||
if (driverId.trim() !== '') {
|
||
driverAgg.set(driverId, (driverAgg.get(driverId) ?? 0) + 1)
|
||
driverFeeAgg.set(driverId, (driverFeeAgg.get(driverId) ?? 0) + fee)
|
||
driverTimeAgg.set(driverId, (driverTimeAgg.get(driverId) ?? 0) + diffMin)
|
||
}
|
||
}
|
||
|
||
// dayAgg -> trendList
|
||
const days = Array.from(dayAgg.keys()).sort()
|
||
for (let i = 0; i < days.length; i++) {
|
||
const day = days[i]
|
||
const obj = dayAgg.get(day)
|
||
if (obj != null) {
|
||
const completed = obj.getNumber('completed_orders') ?? 0
|
||
const sumMin = obj.getNumber('sum_minutes') ?? 0
|
||
const totalFee = obj.getNumber('total_fee') ?? 0
|
||
const out = new UTSJSONObject()
|
||
out.set('day', day)
|
||
out.set('avg_delivery_time', completed > 0 ? sumMin / completed : 0)
|
||
out.set('total_fee', totalFee)
|
||
out.set('completed_orders', completed)
|
||
trendList.push(out)
|
||
}
|
||
}
|
||
|
||
// driverAgg -> topList (Top10)
|
||
const drivers = Array.from(driverAgg.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10)
|
||
for (let i = 0; i < drivers.length; i++) {
|
||
const [driverId, orders] = drivers[i]
|
||
const out = new UTSJSONObject()
|
||
out.set('driver_id', driverId)
|
||
out.set('orders', orders)
|
||
out.set('total_fee', driverFeeAgg.get(driverId) ?? 0)
|
||
out.set('total_minutes', driverTimeAgg.get(driverId) ?? 0)
|
||
topList.push(out)
|
||
}
|
||
} else if (trendRes.error != null) {
|
||
throw trendRes.error
|
||
} else {
|
||
const anyData = trendRes.data as any
|
||
trendList = Array.isArray(anyData) ? (anyData as Array<UTSJSONObject>) : []
|
||
|
||
// Top drivers
|
||
const topRes = await supa.rpc('rpc_delivery_efficiency_top_drivers', {
|
||
p_start: startIso,
|
||
p_end: endIso,
|
||
p_limit: 10
|
||
})
|
||
|
||
if (topRes.status === 404) {
|
||
console.warn('rpc_delivery_efficiency_top_drivers not found, top drivers will be empty')
|
||
} else if (topRes.error != null) {
|
||
throw topRes.error
|
||
} else {
|
||
const topAny = topRes.data as any
|
||
topList = Array.isArray(topAny) ? (topAny as Array<UTSJSONObject>) : []
|
||
}
|
||
}
|
||
|
||
return { trendList, topList, startIso, endIso }
|
||
}
|