mall数据库文件
This commit is contained in:
21
services/analytics/auth.uts
Normal file
21
services/analytics/auth.uts
Normal file
@@ -0,0 +1,21 @@
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
export function getUserIdOrNull(): string | null {
|
||||
try {
|
||||
const sessionInfo = supa.getSession()
|
||||
if (sessionInfo == null || sessionInfo.user == null) return null
|
||||
const userObj = sessionInfo.user as any
|
||||
if (userObj.getString != null) {
|
||||
return (userObj.getString('id') as string | null)
|
||||
}
|
||||
if (userObj.id != null) {
|
||||
return String(userObj.id)
|
||||
}
|
||||
if (typeof userObj === 'object' && 'id' in userObj) {
|
||||
return String((userObj as any).id)
|
||||
}
|
||||
return null
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
41
services/analytics/couponAnalysisService.uts
Normal file
41
services/analytics/couponAnalysisService.uts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { computeDateRange } from './dateRange.uts'
|
||||
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
|
||||
|
||||
export type CouponAnalysisData = {
|
||||
overviewRow: UTSJSONObject | null
|
||||
typeList: Array<UTSJSONObject>
|
||||
channelList: Array<UTSJSONObject>
|
||||
trendList: Array<UTSJSONObject>
|
||||
conversionList: Array<UTSJSONObject>
|
||||
}
|
||||
|
||||
export async function fetchCouponAnalysis(period: string): Promise<CouponAnalysisData> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
|
||||
const overviewRow = await rpcOrNull('rpc_coupon_effectiveness_overview', {
|
||||
p_start: startIso,
|
||||
p_end: endIso
|
||||
} as UTSJSONObject)
|
||||
|
||||
const typeList = await rpcOrEmptyArray('rpc_coupon_type_stats', {
|
||||
p_start: startIso,
|
||||
p_end: endIso
|
||||
} as UTSJSONObject)
|
||||
|
||||
const channelList = await rpcOrEmptyArray('rpc_coupon_channel_stats', {
|
||||
p_start: startIso,
|
||||
p_end: endIso
|
||||
} as UTSJSONObject)
|
||||
|
||||
const trendList = await rpcOrEmptyArray('rpc_coupon_trend_daily', {
|
||||
p_start: startIso,
|
||||
p_end: endIso
|
||||
} as UTSJSONObject)
|
||||
|
||||
const conversionList = await rpcOrEmptyArray('rpc_coupon_conversion_effect', {
|
||||
p_start: startIso,
|
||||
p_end: endIso
|
||||
} as UTSJSONObject)
|
||||
|
||||
return { overviewRow, typeList, channelList, trendList, conversionList }
|
||||
}
|
||||
87
services/analytics/customReportService.uts
Normal file
87
services/analytics/customReportService.uts
Normal file
@@ -0,0 +1,87 @@
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import { rpcOrValue } from './rpc.uts'
|
||||
|
||||
export type CustomReportListItem = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
period: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export type CreateCustomReportParams = {
|
||||
title: string
|
||||
description: string
|
||||
period: string
|
||||
metrics: Array<string>
|
||||
chartType: string
|
||||
}
|
||||
|
||||
export type UpdateCustomReportParams = {
|
||||
reportId: string
|
||||
title: string
|
||||
description: string | null
|
||||
period: string | null
|
||||
}
|
||||
|
||||
export async function listCustomReports(ownerUserId: string): Promise<Array<CustomReportListItem>> {
|
||||
const res: any = await supa
|
||||
.from('analytics_reports')
|
||||
.select('id, title, description, period, updated_at')
|
||||
.eq('type', 'custom')
|
||||
.eq('owner_user_id', ownerUserId)
|
||||
.order('updated_at', { ascending: false } as any)
|
||||
|
||||
if (res?.error != null) {
|
||||
throw res.error
|
||||
}
|
||||
|
||||
const rows: Array<any> = Array.isArray(res.data) ? (res.data as Array<any>) : []
|
||||
const list: Array<CustomReportListItem> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i]
|
||||
list.push({
|
||||
id: `${r.id}`,
|
||||
title: `${r.title}`,
|
||||
description: `${r.description || ''}`,
|
||||
period: `${r.period || ''}`,
|
||||
updated_at: `${r.updated_at || ''}`
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
export async function createCustomReport(params: CreateCustomReportParams): Promise<string> {
|
||||
const data = await rpcOrValue('rpc_create_custom_report', {
|
||||
p_title: params.title,
|
||||
p_description: params.description || '',
|
||||
p_period: params.period,
|
||||
p_metrics: params.metrics,
|
||||
p_chart_type: params.chartType || 'line'
|
||||
} as UTSJSONObject)
|
||||
|
||||
if (data == null) {
|
||||
throw new Error('保存失败:未返回报表ID')
|
||||
}
|
||||
|
||||
return `${data}`
|
||||
}
|
||||
|
||||
export async function updateCustomReport(params: UpdateCustomReportParams): Promise<boolean> {
|
||||
await rpcOrValue('rpc_update_custom_report', {
|
||||
p_report_id: params.reportId,
|
||||
p_title: params.title,
|
||||
p_description: params.description,
|
||||
p_period: params.period
|
||||
} as UTSJSONObject)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function deleteCustomReport(reportId: string): Promise<boolean> {
|
||||
await rpcOrValue('rpc_delete_custom_report', {
|
||||
p_report_id: reportId
|
||||
} as UTSJSONObject)
|
||||
|
||||
return true
|
||||
}
|
||||
151
services/analytics/dashboardService.uts
Normal file
151
services/analytics/dashboardService.uts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { computeDateRange, toDateOnly } from './dateRange.uts'
|
||||
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
|
||||
|
||||
export type TrendData = { x: Array<string>; gmv: Array<number>; orders: Array<number> }
|
||||
export type SegmentItem = { name: string; value: number }
|
||||
export type TrafficItem = { name: string; value: number }
|
||||
export type TopProductItem = { id: string; rank: number; name: string; sales: number }
|
||||
export type TopMerchantItem = { id: string; rank: number; name: string; sales: number; growth: number }
|
||||
|
||||
function safeNumber(v: any): number {
|
||||
const n = Number(v)
|
||||
return isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
export async function fetchDashboardTrend(period: string): Promise<TrendData> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const p_start_date = toDateOnly(startIso)
|
||||
const p_end_date = toDateOnly(endIso)
|
||||
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_trend_data', {
|
||||
p_start_date,
|
||||
p_end_date,
|
||||
p_merchant_id: null
|
||||
} as any)
|
||||
|
||||
const x: Array<string> = []
|
||||
const gmv: Array<number> = []
|
||||
const orders: Array<number> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row: any = rows[i]
|
||||
const d = `${row.getString?.('date') ?? row.getString?.('day') ?? row.getString?.('date_key') ?? ''}`
|
||||
if (d && d.length >= 10) x.push(d.slice(5))
|
||||
else x.push(`${i + 1}`)
|
||||
gmv.push(safeNumber(row.getAny?.('gmv') ?? row.getAny?.('total_amount') ?? 0))
|
||||
orders.push(safeNumber(row.getAny?.('orders') ?? row.getAny?.('order_count') ?? 0))
|
||||
}
|
||||
return { x, gmv, orders }
|
||||
}
|
||||
|
||||
export async function fetchDashboardRealtime(): Promise<any> {
|
||||
const now = new Date()
|
||||
const today0 = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const todayISO = today0.toISOString()
|
||||
|
||||
const ySame = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
const y0 = new Date(ySame.getFullYear(), ySame.getMonth(), ySame.getDate())
|
||||
|
||||
const row = await rpcOrNull('rpc_analytics_realtime_kpis', {
|
||||
p_start: todayISO,
|
||||
p_end: now.toISOString(),
|
||||
p_compare_start: y0.toISOString(),
|
||||
p_compare_end: ySame.toISOString(),
|
||||
p_merchant_id: null
|
||||
} as any)
|
||||
|
||||
const safe = (v: any): number => {
|
||||
const n = Number(v)
|
||||
return isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
const obj: any = row != null ? row : ({} as any)
|
||||
return {
|
||||
gmv: Math.round(safe(obj.getAny?.('gmv') ?? obj.getAny?.('total_gmv') ?? obj.getAny?.('revenue') ?? 0)),
|
||||
gmv_growth: safe(obj.getAny?.('gmv_growth') ?? obj.getAny?.('gmv_growth_rate') ?? obj.getAny?.('revenue_growth') ?? 0),
|
||||
orders: Math.round(safe(obj.getAny?.('orders') ?? obj.getAny?.('order_count') ?? obj.getAny?.('total_orders') ?? 0)),
|
||||
order_growth: safe(obj.getAny?.('order_growth') ?? obj.getAny?.('order_growth_rate') ?? 0),
|
||||
online_users: Math.round(safe(obj.getAny?.('online_users') ?? obj.getAny?.('active_users') ?? obj.getAny?.('current_users') ?? 0)),
|
||||
conversion_rate: safe(obj.getAny?.('conversion_rate') ?? obj.getAny?.('conversion') ?? 0),
|
||||
conversion_growth: safe(obj.getAny?.('conversion_growth') ?? obj.getAny?.('conversion_growth_rate') ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDashboardTopProducts(period: string, limit: number = 50): Promise<Array<TopProductItem>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_top_products', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso),
|
||||
p_limit: limit,
|
||||
p_merchant_id: null
|
||||
} as any)
|
||||
|
||||
const list: Array<TopProductItem> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row: any = rows[i]
|
||||
list.push({
|
||||
id: `${row.getAny?.('id') ?? i}`,
|
||||
rank: i + 1,
|
||||
name: `${row.getAny?.('name') ?? '未知商品'}`,
|
||||
sales: safeNumber(row.getAny?.('sales') ?? row.getAny?.('total_amount') ?? 0)
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
export async function fetchDashboardTopMerchants(period: string, limit: number = 50): Promise<Array<TopMerchantItem>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_top_merchants', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso),
|
||||
p_limit: limit
|
||||
} as any)
|
||||
|
||||
const list: Array<TopMerchantItem> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row: any = rows[i]
|
||||
list.push({
|
||||
id: `${row.getAny?.('id') ?? i}`,
|
||||
rank: i + 1,
|
||||
name: `${row.getAny?.('name') ?? row.getAny?.('shop_name') ?? '未知商家'}`,
|
||||
sales: safeNumber(row.getAny?.('sales') ?? row.getAny?.('total_amount') ?? 0),
|
||||
growth: safeNumber(row.getAny?.('growth') ?? row.getAny?.('growth_rate') ?? 0)
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
export async function fetchDashboardUserSegments(period: string): Promise<Array<SegmentItem>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_user_segments', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso)
|
||||
} as any)
|
||||
|
||||
const list: Array<SegmentItem> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row: any = rows[i]
|
||||
list.push({
|
||||
name: `${row.getAny?.('name') ?? row.getAny?.('segment_name') ?? row.getAny?.('label') ?? '未知'}`,
|
||||
value: safeNumber(row.getAny?.('value') ?? row.getAny?.('count') ?? row.getAny?.('amount') ?? 0)
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
export async function fetchDashboardTrafficSources(period: string): Promise<Array<TrafficItem>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_traffic_sources', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso)
|
||||
} as any)
|
||||
|
||||
const list: Array<TrafficItem> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row: any = rows[i]
|
||||
list.push({
|
||||
name: `${row.getAny?.('name') ?? row.getAny?.('source_name') ?? row.getAny?.('label') ?? '未知'}`,
|
||||
value: safeNumber(row.getAny?.('value') ?? row.getAny?.('count') ?? row.getAny?.('amount') ?? 0)
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
70
services/analytics/dataDetailService.uts
Normal file
70
services/analytics/dataDetailService.uts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
|
||||
|
||||
export type DataDetailReportInfo = {
|
||||
period: string
|
||||
}
|
||||
|
||||
export type DataDetailRow = {
|
||||
id: string
|
||||
date: string
|
||||
gmv: number
|
||||
orders: number
|
||||
users: number
|
||||
}
|
||||
|
||||
export type DataDetailDrillItem = {
|
||||
id: string
|
||||
label: string
|
||||
value: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export async function fetchDataDetailReportInfo(reportId: string): Promise<DataDetailReportInfo | null> {
|
||||
const info = await rpcOrNull('rpc_data_detail_report_info', {
|
||||
p_report_id: reportId
|
||||
} as UTSJSONObject)
|
||||
if (info == null) return null
|
||||
return { period: info.getString('period') ?? '' }
|
||||
}
|
||||
|
||||
export async function fetchDataDetailRows(reportId: string, sortBy: string, sortDir: string, limit: number, offset: number): Promise<Array<DataDetailRow>> {
|
||||
const rows = await rpcOrEmptyArray('rpc_data_detail_rows', {
|
||||
p_report_id: reportId,
|
||||
p_sort_by: sortBy,
|
||||
p_sort_dir: sortDir,
|
||||
p_limit: limit,
|
||||
p_offset: offset
|
||||
} as UTSJSONObject)
|
||||
|
||||
const out: Array<DataDetailRow> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i]
|
||||
const dayStr = r.getString('row_date') ?? ''
|
||||
out.push({
|
||||
id: dayStr + '_' + i.toString(),
|
||||
date: dayStr,
|
||||
gmv: r.getNumber('gmv') ?? 0,
|
||||
orders: r.getNumber('orders') ?? 0,
|
||||
users: r.getNumber('users') ?? 0
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export async function fetchDataDetailDrillItems(reportId: string): Promise<Array<DataDetailDrillItem>> {
|
||||
const rows = await rpcOrEmptyArray('rpc_data_detail_drill_items', {
|
||||
p_report_id: reportId
|
||||
} as UTSJSONObject)
|
||||
|
||||
const out: Array<DataDetailDrillItem> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i]
|
||||
out.push({
|
||||
id: `${r.getAny('id') ?? i}`,
|
||||
label: `${r.getString('label') ?? ''}`,
|
||||
value: `${r.getAny('value') ?? ''}`,
|
||||
type: `${r.getString('type') ?? ''}`
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
15
services/analytics/dateRange.uts
Normal file
15
services/analytics/dateRange.uts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type DateRange = { startIso: string; endIso: string }
|
||||
|
||||
export function computeDateRange(period: string): DateRange {
|
||||
const now = new Date()
|
||||
const start = new Date(now.getTime())
|
||||
if (period === '7d') start.setDate(start.getDate() - 7)
|
||||
else if (period === '30d') start.setDate(start.getDate() - 30)
|
||||
else if (period === '90d') start.setDate(start.getDate() - 90)
|
||||
else if (period === '1y') start.setFullYear(start.getFullYear() - 1)
|
||||
return { startIso: start.toISOString(), endIso: now.toISOString() }
|
||||
}
|
||||
|
||||
export function toDateOnly(iso: string): string {
|
||||
return iso.length >= 10 ? iso.substring(0, 10) : iso
|
||||
}
|
||||
132
services/analytics/deliveryAnalysisService.uts
Normal file
132
services/analytics/deliveryAnalysisService.uts
Normal file
@@ -0,0 +1,132 @@
|
||||
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 }
|
||||
}
|
||||
53
services/analytics/errorMapper.uts
Normal file
53
services/analytics/errorMapper.uts
Normal file
@@ -0,0 +1,53 @@
|
||||
export type AnalyticsErrorContext = {
|
||||
action?: string
|
||||
fallbackMessage?: string
|
||||
}
|
||||
|
||||
export function mapAnalyticsError(err: any, ctx?: AnalyticsErrorContext): string {
|
||||
const fallback = ctx?.fallbackMessage ?? '操作失败'
|
||||
|
||||
try {
|
||||
if (err == null) return fallback
|
||||
|
||||
// string
|
||||
if (typeof err === 'string') {
|
||||
const s = err.trim()
|
||||
return s.length > 0 ? s : fallback
|
||||
}
|
||||
|
||||
// Error
|
||||
const eAny = err as any
|
||||
const msg: string = (eAny?.message != null ? String(eAny.message) : '')
|
||||
const code: string = (eAny?.code != null ? String(eAny.code) : '')
|
||||
const status: number | null = (typeof eAny?.status === 'number' ? (eAny.status as number) : null)
|
||||
|
||||
// RPC not found / route not found
|
||||
if (status === 404) {
|
||||
return '功能尚未部署(RPC 未创建)'
|
||||
}
|
||||
|
||||
// auth
|
||||
if (code === 'P0001' || msg.includes('用户未登录') || msg.toLowerCase().includes('not logged') || msg.toLowerCase().includes('jwt')) {
|
||||
return '请先登录'
|
||||
}
|
||||
|
||||
// permission
|
||||
if (msg.includes('无权限') || msg.toLowerCase().includes('permission') || msg.toLowerCase().includes('forbidden')) {
|
||||
return '无权限操作'
|
||||
}
|
||||
|
||||
// not found
|
||||
if (msg.includes('不存在') || msg.toLowerCase().includes('not found')) {
|
||||
return '数据不存在或已删除'
|
||||
}
|
||||
|
||||
// fallback to message
|
||||
if (msg.trim().length > 0) {
|
||||
return msg
|
||||
}
|
||||
|
||||
return fallback
|
||||
} catch (e) {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
67
services/analytics/insightDetailService.uts
Normal file
67
services/analytics/insightDetailService.uts
Normal file
@@ -0,0 +1,67 @@
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
export type InsightDetail = {
|
||||
id: string
|
||||
report_id: string
|
||||
type: string
|
||||
impact: string
|
||||
title: string
|
||||
content: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type RelatedReport = {
|
||||
id: string
|
||||
title: string
|
||||
type: string
|
||||
period: string
|
||||
generated_at: string
|
||||
}
|
||||
|
||||
export async function fetchInsightDetail(insightId: string): Promise<InsightDetail | null> {
|
||||
const res: any = await supa
|
||||
.from('analytics_insights')
|
||||
.select('id, report_id, type, impact, title, content, created_at')
|
||||
.eq('id', insightId)
|
||||
.single()
|
||||
|
||||
if (res?.error != null) {
|
||||
throw res.error
|
||||
}
|
||||
|
||||
const it: any = res.data
|
||||
if (it == null) return null
|
||||
|
||||
return {
|
||||
id: `${it.id}`,
|
||||
report_id: `${it.report_id || ''}`,
|
||||
type: `${it.type || 'info'}`,
|
||||
impact: `${it.impact || 'medium'}`,
|
||||
title: `${it.title || ''}`,
|
||||
content: `${it.content || ''}`,
|
||||
created_at: `${it.created_at || ''}`
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchRelatedReport(reportId: string): Promise<RelatedReport | null> {
|
||||
const rRes: any = await supa
|
||||
.from('analytics_reports')
|
||||
.select('id, title, type, period, generated_at')
|
||||
.eq('id', reportId)
|
||||
.single()
|
||||
|
||||
if (rRes?.error != null) {
|
||||
throw rRes.error
|
||||
}
|
||||
|
||||
const r: any = rRes.data
|
||||
if (r == null) return null
|
||||
|
||||
return {
|
||||
id: `${r.id}`,
|
||||
title: `${r.title}`,
|
||||
type: `${r.type}`,
|
||||
period: `${r.period}`,
|
||||
generated_at: `${r.generated_at || ''}`
|
||||
}
|
||||
}
|
||||
53
services/analytics/marketTrendsService.uts
Normal file
53
services/analytics/marketTrendsService.uts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { computeDateRange, toDateOnly } from './dateRange.uts'
|
||||
import { rpcOrEmptyArray } from './rpc.uts'
|
||||
|
||||
export type MarketTrendsData = {
|
||||
trendRows: Array<UTSJSONObject>
|
||||
categoryRows: Array<UTSJSONObject>
|
||||
seasonalRows: Array<UTSJSONObject>
|
||||
priceRows: Array<UTSJSONObject>
|
||||
competitionRows: Array<UTSJSONObject>
|
||||
startIso: string
|
||||
endIso: string
|
||||
}
|
||||
|
||||
export async function fetchMarketTrends(period: string): Promise<MarketTrendsData> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const startDate = toDateOnly(startIso)
|
||||
const endDate = toDateOnly(endIso)
|
||||
|
||||
const trendRows = await rpcOrEmptyArray('rpc_analytics_market_trend_daily', {
|
||||
p_start: startIso,
|
||||
p_end: endIso
|
||||
} as UTSJSONObject)
|
||||
|
||||
const categoryRows = await rpcOrEmptyArray('rpc_analytics_category_sales', {
|
||||
p_start_date: startDate,
|
||||
p_end_date: endDate
|
||||
} as UTSJSONObject)
|
||||
|
||||
const seasonalRows = await rpcOrEmptyArray('rpc_analytics_seasonal_trend', {
|
||||
p_start_date: startDate,
|
||||
p_end_date: endDate
|
||||
} as UTSJSONObject)
|
||||
|
||||
const priceRows = await rpcOrEmptyArray('rpc_analytics_price_trend', {
|
||||
p_start: startIso,
|
||||
p_end: endIso
|
||||
} as UTSJSONObject)
|
||||
|
||||
const competitionRows = await rpcOrEmptyArray('rpc_analytics_competition_share', {
|
||||
p_start_date: startDate,
|
||||
p_end_date: endDate
|
||||
} as UTSJSONObject)
|
||||
|
||||
return {
|
||||
trendRows,
|
||||
categoryRows,
|
||||
seasonalRows,
|
||||
priceRows,
|
||||
competitionRows,
|
||||
startIso,
|
||||
endIso
|
||||
}
|
||||
}
|
||||
113
services/analytics/productInsightsService.uts
Normal file
113
services/analytics/productInsightsService.uts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { computeDateRange, toDateOnly } from './dateRange.uts'
|
||||
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
|
||||
|
||||
export type ProductOverview = {
|
||||
total_products: number
|
||||
product_growth: number
|
||||
hot_products: number
|
||||
turnover_rate: number
|
||||
turnover_growth: number
|
||||
avg_stock: number
|
||||
stock_growth: number
|
||||
}
|
||||
|
||||
export type ProductRank = { id: string; rank: number; name: string; sales: number; growth: number }
|
||||
|
||||
export type ProductTrendRow = { date: string; gmv: number; qty: number; orders: number }
|
||||
|
||||
function safeNumber(v: any): number {
|
||||
const n = Number(v)
|
||||
return isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
export async function fetchProductOverview(period: string): Promise<ProductOverview> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const row = await rpcOrNull('rpc_product_insights_overview', {
|
||||
p_start: startIso,
|
||||
p_end: endIso
|
||||
} as any)
|
||||
|
||||
const obj: any = row != null ? row : ({} as any)
|
||||
return {
|
||||
total_products: safeNumber(obj.getAny?.('total_products') ?? 0),
|
||||
product_growth: safeNumber(obj.getAny?.('product_growth') ?? 0),
|
||||
hot_products: safeNumber(obj.getAny?.('hot_products') ?? 0),
|
||||
turnover_rate: safeNumber(obj.getAny?.('turnover_rate') ?? 0),
|
||||
turnover_growth: safeNumber(obj.getAny?.('turnover_growth') ?? 0),
|
||||
avg_stock: safeNumber(obj.getAny?.('avg_stock') ?? 0),
|
||||
stock_growth: safeNumber(obj.getAny?.('stock_growth') ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTopProducts(period: string, limit: number = 10): Promise<Array<ProductRank>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_top_products', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso),
|
||||
p_limit: limit,
|
||||
p_merchant_id: null
|
||||
} as any)
|
||||
|
||||
const list: Array<ProductRank> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r: any = rows[i]
|
||||
list.push({
|
||||
id: `${r.getAny?.('id') ?? i}`,
|
||||
rank: i + 1,
|
||||
name: `${r.getAny?.('name') ?? '未知商品'}`,
|
||||
sales: safeNumber(r.getAny?.('sales') ?? r.getAny?.('total_amount') ?? 0),
|
||||
growth: safeNumber(r.getAny?.('growth') ?? r.getAny?.('growth_rate') ?? 0)
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
export async function fetchProductTrend(period: string, productId: string): Promise<Array<ProductTrendRow>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_product_trend', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso),
|
||||
p_product_id: productId
|
||||
} as any)
|
||||
|
||||
const out: Array<ProductTrendRow> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r: any = rows[i]
|
||||
const date = `${r.getAny?.('date') ?? r.getAny?.('day') ?? r.getAny?.('date_key') ?? ''}`
|
||||
out.push({
|
||||
date,
|
||||
gmv: safeNumber(r.getAny?.('gmv') ?? r.getAny?.('total_amount') ?? 0),
|
||||
qty: safeNumber(r.getAny?.('qty') ?? r.getAny?.('sales_qty') ?? 0),
|
||||
orders: safeNumber(r.getAny?.('orders') ?? r.getAny?.('order_count') ?? 0)
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export async function fetchCategorySales(period: string): Promise<Array<UTSJSONObject>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
return await rpcOrEmptyArray('rpc_analytics_category_sales', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso)
|
||||
} as any)
|
||||
}
|
||||
|
||||
export async function fetchStockInsights(period: string): Promise<Array<UTSJSONObject>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
return await rpcOrEmptyArray('rpc_product_insights_stock', {
|
||||
p_start: startIso,
|
||||
p_end: endIso
|
||||
} as any)
|
||||
}
|
||||
|
||||
export async function fetchPriceTrend(period: string): Promise<Array<UTSJSONObject>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
return await rpcOrEmptyArray('rpc_analytics_price_trend', {
|
||||
p_start: startIso,
|
||||
p_end: endIso
|
||||
} as any)
|
||||
}
|
||||
|
||||
export async function fetchReviewInsights(): Promise<Array<UTSJSONObject>> {
|
||||
return await rpcOrEmptyArray('rpc_product_insights_reviews', {} as any)
|
||||
}
|
||||
146
services/analytics/reportDetailService.uts
Normal file
146
services/analytics/reportDetailService.uts
Normal file
@@ -0,0 +1,146 @@
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
export type AnalyticsReport = {
|
||||
id: string
|
||||
title: string
|
||||
type: string
|
||||
period: string
|
||||
generated_at: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export type AnalyticsReportMetric = {
|
||||
key: string
|
||||
label: string
|
||||
value: number
|
||||
format: string
|
||||
icon: string
|
||||
color: string
|
||||
change: number
|
||||
}
|
||||
|
||||
export type AnalyticsReportRow = {
|
||||
row_date: string
|
||||
gmv: number
|
||||
orders: number
|
||||
users: number
|
||||
conversion: number
|
||||
avg_order_amount: number
|
||||
}
|
||||
|
||||
export type AnalyticsInsight = {
|
||||
id: string
|
||||
type: string
|
||||
title: string
|
||||
content: string
|
||||
impact: string
|
||||
}
|
||||
|
||||
export type AnalyticsRelatedReport = AnalyticsReport
|
||||
|
||||
function safeNumber(v: any): number {
|
||||
const n = Number(v)
|
||||
return isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
export async function fetchReport(reportId: string): Promise<AnalyticsReport | null> {
|
||||
const reportRes: any = await supa
|
||||
.from('analytics_reports')
|
||||
.select('id, title, type, period, generated_at, description')
|
||||
.eq('id', reportId)
|
||||
|
||||
if (reportRes?.error != null) throw reportRes.error
|
||||
|
||||
const rows: Array<any> = Array.isArray(reportRes.data) ? (reportRes.data as Array<any>) : []
|
||||
if (rows.length === 0) return null
|
||||
const r = rows[0]
|
||||
return {
|
||||
id: `${r.id}`,
|
||||
title: `${r.title}`,
|
||||
type: `${r.type}`,
|
||||
period: `${r.period}`,
|
||||
generated_at: `${r.generated_at}`,
|
||||
description: `${r.description || ''}`
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchReportMetrics(reportId: string): Promise<Array<AnalyticsReportMetric>> {
|
||||
const metricRes: any = await supa
|
||||
.from('analytics_report_metrics')
|
||||
.select('metric_key, metric_label, metric_value_num, format, icon, color, change_pct')
|
||||
.eq('report_id', reportId)
|
||||
|
||||
if (metricRes?.error != null) throw metricRes.error
|
||||
|
||||
const metricRows: Array<any> = Array.isArray(metricRes.data) ? (metricRes.data as Array<any>) : []
|
||||
return metricRows.map((m: any) => ({
|
||||
key: `${m.metric_key}`,
|
||||
label: `${m.metric_label}`,
|
||||
value: safeNumber(m.metric_value_num),
|
||||
format: `${m.format || 'number'}`,
|
||||
icon: `${m.icon || '📊'}`,
|
||||
color: `${m.color || '#4caf50'}`,
|
||||
change: safeNumber(m.change_pct)
|
||||
}))
|
||||
}
|
||||
|
||||
export async function fetchReportRows(reportId: string): Promise<Array<AnalyticsReportRow>> {
|
||||
const rowsRes: any = await supa
|
||||
.from('analytics_report_rows')
|
||||
.select('row_date, gmv, orders, users, conversion, avg_order_amount')
|
||||
.eq('report_id', reportId)
|
||||
.order('row_date', { ascending: true } as any)
|
||||
|
||||
if (rowsRes?.error != null) throw rowsRes.error
|
||||
|
||||
const rows: Array<any> = Array.isArray(rowsRes.data) ? (rowsRes.data as Array<any>) : []
|
||||
return rows.map((row: any) => ({
|
||||
row_date: `${row.row_date}`,
|
||||
gmv: safeNumber(row.gmv),
|
||||
orders: safeNumber(row.orders),
|
||||
users: safeNumber(row.users),
|
||||
conversion: safeNumber(row.conversion),
|
||||
avg_order_amount: safeNumber(row.avg_order_amount)
|
||||
}))
|
||||
}
|
||||
|
||||
export async function fetchReportInsights(reportId: string): Promise<Array<AnalyticsInsight>> {
|
||||
const insightRes: any = await supa
|
||||
.from('analytics_insights')
|
||||
.select('id, type, title, content, impact')
|
||||
.eq('report_id', reportId)
|
||||
.order('created_at', { ascending: false } as any)
|
||||
|
||||
if (insightRes?.error != null) throw insightRes.error
|
||||
|
||||
const insRows: Array<any> = Array.isArray(insightRes.data) ? (insightRes.data as Array<any>) : []
|
||||
return insRows.map((it: any) => ({
|
||||
id: `${it.id}`,
|
||||
type: `${it.type || 'info'}`,
|
||||
title: `${it.title}`,
|
||||
content: `${it.content}`,
|
||||
impact: `${it.impact || 'medium'}`
|
||||
}))
|
||||
}
|
||||
|
||||
export async function fetchRelatedReports(reportType: string, excludeReportId: string): Promise<Array<AnalyticsRelatedReport>> {
|
||||
const relatedRes: any = await supa
|
||||
.from('analytics_reports')
|
||||
.select('id, title, type, period, generated_at, description')
|
||||
.eq('type', reportType)
|
||||
.neq('id', excludeReportId)
|
||||
.order('generated_at', { ascending: false } as any)
|
||||
.limit(3 as any)
|
||||
|
||||
if (relatedRes?.error != null) throw relatedRes.error
|
||||
|
||||
const relRows: Array<any> = Array.isArray(relatedRes.data) ? (relatedRes.data as Array<any>) : []
|
||||
return relRows.map((it: any) => ({
|
||||
id: `${it.id}`,
|
||||
title: `${it.title}`,
|
||||
type: `${it.type}`,
|
||||
period: `${it.period}`,
|
||||
generated_at: `${it.generated_at}`,
|
||||
description: `${it.description || ''}`
|
||||
}))
|
||||
}
|
||||
42
services/analytics/rpc.uts
Normal file
42
services/analytics/rpc.uts
Normal file
@@ -0,0 +1,42 @@
|
||||
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
export async function rpcOrEmptyArray(functionName: string, params: UTSJSONObject): Promise<Array<UTSJSONObject>> {
|
||||
await ensureSupabaseReady()
|
||||
const res: any = await supa.rpc(functionName, params)
|
||||
if (res?.status === 404) {
|
||||
return [] as Array<UTSJSONObject>
|
||||
}
|
||||
if (res?.error != null) {
|
||||
throw res.error
|
||||
}
|
||||
const anyData = res.data as any
|
||||
return Array.isArray(anyData) ? (anyData as Array<UTSJSONObject>) : ([] as Array<UTSJSONObject>)
|
||||
}
|
||||
|
||||
export async function rpcOrNull(functionName: string, params: UTSJSONObject): Promise<UTSJSONObject | null> {
|
||||
await ensureSupabaseReady()
|
||||
const res: any = await supa.rpc(functionName, params)
|
||||
if (res?.status === 404) {
|
||||
return null
|
||||
}
|
||||
if (res?.error != null) {
|
||||
throw res.error
|
||||
}
|
||||
const anyData = res.data as any
|
||||
if (Array.isArray(anyData)) {
|
||||
return anyData.length > 0 ? (anyData[0] as UTSJSONObject) : null
|
||||
}
|
||||
return anyData != null ? (anyData as UTSJSONObject) : null
|
||||
}
|
||||
|
||||
export async function rpcOrValue(functionName: string, params: UTSJSONObject): Promise<any> {
|
||||
await ensureSupabaseReady()
|
||||
const res: any = await supa.rpc(functionName, params)
|
||||
if (res?.status === 404) {
|
||||
return null
|
||||
}
|
||||
if (res?.error != null) {
|
||||
throw res.error
|
||||
}
|
||||
return res.data
|
||||
}
|
||||
127
services/analytics/salesReportService.uts
Normal file
127
services/analytics/salesReportService.uts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { computeDateRange, toDateOnly } from './dateRange.uts'
|
||||
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
|
||||
|
||||
export type TrendData = { x: Array<string>; gmv: Array<number>; orders: Array<number> }
|
||||
|
||||
export type SalesKpis = {
|
||||
gmv: number
|
||||
gmv_growth: number
|
||||
orders: number
|
||||
order_growth: number
|
||||
conversion_rate: number
|
||||
conversion_growth: number
|
||||
avg_order_amount: number
|
||||
avg_order_growth: number
|
||||
}
|
||||
|
||||
export type ProductRank = { id: string; rank: number; name: string; sales: number }
|
||||
export type MerchantRank = { id: string; rank: number; name: string; sales: number; growth: number }
|
||||
|
||||
function safeNumber(v: any): number {
|
||||
const n = Number(v)
|
||||
return isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
export async function fetchSalesKpis(period: string): Promise<SalesKpis> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const days = period === '7d' ? 7 : period === '30d' ? 30 : period === '90d' ? 90 : 365
|
||||
|
||||
const startDateObj = new Date(startIso)
|
||||
const endDateObj = new Date(endIso)
|
||||
|
||||
const periodStart = new Date(startDateObj.getFullYear(), startDateObj.getMonth(), startDateObj.getDate())
|
||||
const periodEnd = new Date(endDateObj.getFullYear(), endDateObj.getMonth(), endDateObj.getDate() + 1)
|
||||
const prevStart = new Date(periodStart.getTime() - days * 24 * 60 * 60 * 1000)
|
||||
const prevEnd = new Date(periodStart.getTime())
|
||||
|
||||
const row = await rpcOrNull('rpc_analytics_realtime_kpis', {
|
||||
p_start: periodStart.toISOString(),
|
||||
p_end: periodEnd.toISOString(),
|
||||
p_compare_start: prevStart.toISOString(),
|
||||
p_compare_end: prevEnd.toISOString(),
|
||||
p_merchant_id: null
|
||||
} as any)
|
||||
|
||||
const obj: any = row != null ? row : ({} as any)
|
||||
const gmv = safeNumber(obj.getAny?.('gmv') ?? 0)
|
||||
const orders = safeNumber(obj.getAny?.('orders') ?? 0)
|
||||
const avgOrder = orders > 0 ? gmv / orders : 0
|
||||
|
||||
return {
|
||||
gmv: Math.round(gmv),
|
||||
gmv_growth: safeNumber(obj.getAny?.('gmv_growth') ?? 0),
|
||||
orders: Math.round(orders),
|
||||
order_growth: safeNumber(obj.getAny?.('order_growth') ?? 0),
|
||||
conversion_rate: safeNumber(obj.getAny?.('conversion_rate') ?? 0),
|
||||
conversion_growth: safeNumber(obj.getAny?.('conversion_growth') ?? 0),
|
||||
avg_order_amount: avgOrder,
|
||||
avg_order_growth: safeNumber(obj.getAny?.('avg_order_growth') ?? obj.getAny?.('gmv_growth') ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSalesTrend(period: string): Promise<TrendData> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_trend_data', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso),
|
||||
p_merchant_id: null
|
||||
} as any)
|
||||
|
||||
const x: Array<string> = []
|
||||
const gmvArr: Array<number> = []
|
||||
const orderArr: Array<number> = []
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r: any = rows[i]
|
||||
const d = `${r.getAny?.('date') ?? ''}`
|
||||
x.push(d.length >= 10 ? d.slice(5) : d)
|
||||
gmvArr.push(safeNumber(r.getAny?.('gmv') ?? 0))
|
||||
orderArr.push(safeNumber(r.getAny?.('orders') ?? 0))
|
||||
}
|
||||
|
||||
return { x, gmv: gmvArr, orders: orderArr }
|
||||
}
|
||||
|
||||
export async function fetchSalesTopProducts(period: string, limit: number = 50): Promise<Array<ProductRank>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_top_products', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso),
|
||||
p_limit: limit,
|
||||
p_merchant_id: null
|
||||
} as any)
|
||||
|
||||
const list: Array<ProductRank> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r: any = rows[i]
|
||||
list.push({
|
||||
id: `${r.getAny?.('id') ?? i}`,
|
||||
rank: i + 1,
|
||||
name: `${r.getAny?.('name') ?? ''}`,
|
||||
sales: safeNumber(r.getAny?.('sales') ?? 0)
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
export async function fetchSalesTopMerchants(period: string, limit: number = 50): Promise<Array<MerchantRank>> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_top_merchants', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso),
|
||||
p_limit: limit
|
||||
} as any)
|
||||
|
||||
const list: Array<MerchantRank> = []
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r: any = rows[i]
|
||||
list.push({
|
||||
id: `${r.getAny?.('id') ?? i}`,
|
||||
rank: i + 1,
|
||||
name: `${r.getAny?.('name') ?? ''}`,
|
||||
sales: safeNumber(r.getAny?.('sales') ?? 0),
|
||||
growth: safeNumber(r.getAny?.('growth') ?? 0)
|
||||
})
|
||||
}
|
||||
return list
|
||||
}
|
||||
121
services/analytics/userAnalysisService.uts
Normal file
121
services/analytics/userAnalysisService.uts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { computeDateRange, toDateOnly } from './dateRange.uts'
|
||||
import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts'
|
||||
|
||||
// --- Type Definitions ---
|
||||
|
||||
export type UserKpis = {
|
||||
total_users: number
|
||||
user_growth: number
|
||||
new_users: number
|
||||
new_user_growth: number
|
||||
active_users: number // DAU
|
||||
active_growth: number
|
||||
paid_users: number
|
||||
paid_growth: number
|
||||
new_user_conversion_rate: number
|
||||
repurchase_rate: number
|
||||
}
|
||||
|
||||
export type UserGrowthTrend = {
|
||||
dates: Array<string>
|
||||
newUsers: Array<number>
|
||||
activeUsers: Array<number>
|
||||
}
|
||||
|
||||
export type UserActivity = {
|
||||
dau: number
|
||||
wau: number
|
||||
mau: number
|
||||
}
|
||||
|
||||
export type UserRetention = {
|
||||
days: Array<string> // e.g., '次日', '3日', '7日', '14日', '30日'
|
||||
rates: Array<number>
|
||||
}
|
||||
|
||||
export type NewVsOldComparison = {
|
||||
categories: Array<string> // e.g., 'GMV', '订单数', '客单价'
|
||||
newUserData: Array<number>
|
||||
oldUserData: Array<number>
|
||||
}
|
||||
|
||||
// --- Helper ---
|
||||
|
||||
function safeNumber(v: any): number {
|
||||
const n = Number(v)
|
||||
return isFinite(n) ? n : 0
|
||||
}
|
||||
|
||||
// --- Service Functions ---
|
||||
|
||||
export async function fetchUserKpis(period: string): Promise<UserKpis> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const row = await rpcOrNull('rpc_analytics_user_kpis', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso)
|
||||
} as any)
|
||||
|
||||
const obj: any = row != null ? row : ({} as any)
|
||||
return {
|
||||
total_users: safeNumber(obj.getAny?.('total_users') ?? 0),
|
||||
user_growth: safeNumber(obj.getAny?.('user_growth') ?? 0),
|
||||
new_users: safeNumber(obj.getAny?.('new_users') ?? 0),
|
||||
new_user_growth: safeNumber(obj.getAny?.('new_user_growth') ?? 0),
|
||||
active_users: safeNumber(obj.getAny?.('active_users') ?? 0),
|
||||
active_growth: safeNumber(obj.getAny?.('active_growth') ?? 0),
|
||||
paid_users: safeNumber(obj.getAny?.('paid_users') ?? 0), // Placeholder
|
||||
paid_growth: safeNumber(obj.getAny?.('paid_growth') ?? 0), // Placeholder
|
||||
new_user_conversion_rate: safeNumber(obj.getAny?.('new_user_conversion_rate') ?? 0), // Placeholder
|
||||
repurchase_rate: safeNumber(obj.getAny?.('repurchase_rate') ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchUserGrowthTrend(period: string): Promise<UserGrowthTrend> {
|
||||
const { startIso, endIso } = computeDateRange(period)
|
||||
const rows = await rpcOrEmptyArray('rpc_analytics_user_growth_trend', {
|
||||
p_start_date: toDateOnly(startIso),
|
||||
p_end_date: toDateOnly(endIso)
|
||||
} as any)
|
||||
|
||||
const dates: Array<string> = []
|
||||
const newUsers: Array<number> = []
|
||||
const activeUsers: Array<number> = []
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r: any = rows[i]
|
||||
const d = `${r.getAny?.('date') ?? ''}`
|
||||
dates.push(d.length >= 10 ? d.slice(5) : d)
|
||||
newUsers.push(safeNumber(r.getAny?.('new_users') ?? 0))
|
||||
activeUsers.push(safeNumber(r.getAny?.('active_users') ?? 0))
|
||||
}
|
||||
|
||||
return { dates, newUsers, activeUsers }
|
||||
}
|
||||
|
||||
// Placeholder for functions that need new RPCs
|
||||
|
||||
export async function fetchUserActivity(period: string): Promise<UserActivity> {
|
||||
console.warn('fetchUserActivity needs rpc_analytics_user_activity RPC')
|
||||
return { dau: 0, wau: 0, mau: 0 }
|
||||
}
|
||||
|
||||
export async function fetchUserRetention(period: string): Promise<UserRetention> {
|
||||
console.warn('fetchUserRetention needs rpc_analytics_user_retention RPC')
|
||||
return { days: ['次日', '3日', '7日', '14日', '30日'], rates: [0, 0, 0, 0, 0] }
|
||||
}
|
||||
|
||||
export async function fetchNewVsOldComparison(period: string): Promise<NewVsOldComparison> {
|
||||
console.warn('fetchNewVsOldComparison needs rpc_analytics_new_vs_old_users RPC')
|
||||
return { categories: ['GMV', '订单数', '客单价'], newUserData: [0, 0, 0], oldUserData: [0, 0, 0] }
|
||||
}
|
||||
|
||||
export async function fetchConversionFunnel(period: string): Promise<Array<{ step: string; value: number }>> {
|
||||
console.warn('fetchConversionFunnel needs rpc_analytics_conversion_funnel RPC')
|
||||
return [
|
||||
{ step: '访问', value: 0 },
|
||||
{ step: '详情页', value: 0 },
|
||||
{ step: '加购', value: 0 },
|
||||
{ step: '下单', value: 0 },
|
||||
{ step: '支付', value: 0 }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user