- - - - {{ p.label }} + + + + 时间范围 + + + {{ p.label }} + + + + + + 渠道/终端/会员/新老:待接入数据后开放 - - + + - 总用户数 - {{ formatInt(userData.total_users) }} - 较上期:{{ formatPct(userData.user_growth) }} - - - 新用户 + 新增用户 {{ formatInt(userData.new_users) }} 较上期:{{ formatPct(userData.new_user_growth) }} - 用户活跃度 - {{ formatPct(userData.active_rate) }} + 活跃用户(DAU) + {{ formatInt(userData.active_users) }} 较上期:{{ formatPct(userData.active_growth) }} + + 下单用户数 + {{ formatInt(userData.ordering_users) }} + 较上期:{{ formatPct(userData.ordering_growth) }} + + + 支付用户数 + {{ formatInt(userData.paid_users) }} + 较上期:{{ formatPct(userData.paid_growth) }} + + + 新客转化率 + {{ formatPct(userData.new_user_conversion_rate) }} + 新客 → 下单/支付 + 复购率 {{ formatPct(userData.repurchase_rate) }} @@ -64,11 +83,11 @@ - + - 用户增长趋势 - {{ selectedPeriodText }} · 新用户 vs 总用户 + 增长与活跃趋势 + {{ selectedPeriodText }} · 新增 vs 活跃(DAU) {{ loading ? '加载中...' : '暂无数据' }} @@ -76,60 +95,110 @@ - - - - 用户洞察 - 留存率 / 新老对比 / 活跃度 / 画像 + + + + 新客转化趋势 + 新客 → 下单/支付(待接入) + + + 暂无数据 / 待接入 + - - - - 用户留存率 - 按留存天数统计 - - + + + + 回访 / 复购趋势 + 复购人数 / 复购率(待接入) - - - 新老用户对比 - GMV、订单数、客单价 - - - - - - 用户活跃度 - 日活/周活/月活 - - - - - - 用户画像 - 性别/年龄/地域 - - + + 暂无数据 / 待接入 - + - 用户分群 & 流量来源 - {{ selectedPeriodText }} · 分群占比 & 来源分布 + 转化漏斗 + 拉新 → 激活 → 转化(待接入埋点/事件) + + + + + + {{ idx + 1 }} + {{ s.step }} + + + {{ formatInt(s.value) }} + 转化:{{ formatPct(calcFunnelRate(idx)) }} + + + + + + + 暂无漏斗数据 / 待接入:UV、PDP、加购、下单、支付 + + + + + + + + 留存与回访 + {{ selectedPeriodText }} · 1/3/7/14/30日留存(Cohort 后续补) + + + + + 留存曲线 + 留存率趋势(待接入) + + + + + + 流失用户占比 + 7/14天未活跃(待接入) + + + 暂无数据 / 待接入 + + + + + + + + + 用户分群(运营可用) + RFM / LTV / 新客分层(后续补) · 当前为基础结构占比 - + + 用户画像(基础) + 性别/年龄/地域(待接入) + + + + + + 渠道来源 + {{ selectedPeriodText }} · 渠道占比(后续可扩展渠道质量表) + + + + @@ -138,272 +207,321 @@ - diff --git a/services/analytics/couponAnalysisService.uts b/services/analytics/couponAnalysisService.uts index d7cae687..960195b0 100644 --- a/services/analytics/couponAnalysisService.uts +++ b/services/analytics/couponAnalysisService.uts @@ -1,4 +1,4 @@ -import { computeDateRange } from './dateRange.uts' +import { computeDateRange, toDateOnly } from './dateRange.uts' import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts' export type CouponAnalysisData = { @@ -11,31 +11,21 @@ export type CouponAnalysisData = { export async function fetchCouponAnalysis(period: string): Promise { const { startIso, endIso } = computeDateRange(period) + const p_start_date = toDateOnly(startIso) + const p_end_date = toDateOnly(endIso) - const overviewRow = await rpcOrNull('rpc_coupon_effectiveness_overview', { - p_start: startIso, - p_end: endIso - } as UTSJSONObject) + const params = { + p_start_date, + p_end_date + } as any - 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) + const [overviewRow, typeList, channelList, trendList, conversionList] = await Promise.all([ + rpcOrNull('rpc_analytics_coupon_overview', params), + rpcOrEmptyArray('rpc_analytics_coupon_by_type', params), + rpcOrEmptyArray('rpc_analytics_coupon_by_channel', params), + rpcOrEmptyArray('rpc_analytics_coupon_trend', params), + rpcOrEmptyArray('rpc_analytics_coupon_conversion', params) + ]) return { overviewRow, typeList, channelList, trendList, conversionList } } diff --git a/services/analytics/customReportService.uts b/services/analytics/customReportService.uts index 0e8efcc9..b5ddc587 100644 --- a/services/analytics/customReportService.uts +++ b/services/analytics/customReportService.uts @@ -1,5 +1,4 @@ -import supa from '@/components/supadb/aksupainstance.uts' -import { rpcOrValue } from './rpc.uts' +import { rpcOrEmptyArray, rpcOrValue } from './rpc.uts' export type CustomReportListItem = { id: string @@ -24,41 +23,41 @@ export type UpdateCustomReportParams = { period: string | null } +function safeString(v: any): string { + return v != null ? `${v}` : '' +} + +// 改造:不再直查 analytics_reports 表,统一通过 RPC 获取当前用户的报表列表 export async function listCustomReports(ownerUserId: string): Promise> { - 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 = Array.isArray(res.data) ? (res.data as Array) : [] + const rows = await rpcOrEmptyArray('rpc_get_custom_reports', {} as any) const list: Array = [] for (let i = 0; i < rows.length; i++) { - const r = rows[i] + const r: any = rows[i] list.push({ - id: `${r.id}`, - title: `${r.title}`, - description: `${r.description || ''}`, - period: `${r.period || ''}`, - updated_at: `${r.updated_at || ''}` + id: safeString(r.getAny?.('id') ?? r.getString?.('id')), + title: safeString(r.getAny?.('title') ?? r.getString?.('title')), + description: safeString(r.getAny?.('description') ?? r.getString?.('description')), + // 兼容旧 UI 字段:custom-report 页面里可能还在用 period 字段 + period: '', + updated_at: safeString(r.getAny?.('updated_at') ?? r.getString?.('updated_at')) }) } return list } +// 改造:RPC 参数改为 p_definition(JSONB),承载 period/metrics/chartType export async function createCustomReport(params: CreateCustomReportParams): Promise { + const definition = { + period: params.period, + metrics: params.metrics, + chartType: params.chartType || 'line' + } + 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) + p_definition: definition + } as any) if (data == null) { throw new Error('保存失败:未返回报表ID') @@ -68,12 +67,17 @@ export async function createCustomReport(params: CreateCustomReportParams): Prom } export async function updateCustomReport(params: UpdateCustomReportParams): Promise { + // 注意:旧 UI 只传 title/description/period,这里把 period 合并进 definition + const definition = { + period: params.period + } + 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) + p_definition: definition + } as any) return true } @@ -81,7 +85,7 @@ export async function updateCustomReport(params: UpdateCustomReportParams): Prom export async function deleteCustomReport(reportId: string): Promise { await rpcOrValue('rpc_delete_custom_report', { p_report_id: reportId - } as UTSJSONObject) + } as any) return true } diff --git a/services/analytics/dashboardService.uts b/services/analytics/dashboardService.uts index c8ece9ee..453e212b 100644 --- a/services/analytics/dashboardService.uts +++ b/services/analytics/dashboardService.uts @@ -1,5 +1,5 @@ import { computeDateRange, toDateOnly } from './dateRange.uts' -import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts' +import { rpcOrEmptyArray, rpcOrNull, rpcOrValue } from './rpc.uts' export type TrendData = { x: Array; gmv: Array; orders: Array } export type SegmentItem = { name: string; value: number } @@ -17,10 +17,9 @@ export async function fetchDashboardTrend(period: string): Promise { const p_start_date = toDateOnly(startIso) const p_end_date = toDateOnly(endIso) - const rows = await rpcOrEmptyArray('rpc_analytics_trend_data', { + const rows = await rpcOrEmptyArray('rpc_analytics_sales_trend', { p_start_date, - p_end_date, - p_merchant_id: null + p_end_date } as any) const x: Array = [] @@ -28,45 +27,30 @@ export async function fetchDashboardTrend(period: string): Promise { const orders: Array = [] 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)) + const d = `${row.getAny?.('date') ?? ''}` + x.push(d.length >= 10 ? d.slice(5) : d) + gmv.push(safeNumber(row.getAny?.('gmv') ?? 0)) + orders.push(safeNumber(row.getAny?.('orders') ?? 0)) } return { x, gmv, orders } } export async function fetchDashboardRealtime(): Promise { - const now = new Date() - const today0 = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - const todayISO = today0.toISOString() + const [kpiRow, onlineUsersVal] = await Promise.all([ + rpcOrNull('rpc_analytics_realtime_kpis', {} as any), + rpcOrValue('rpc_analytics_online_users', {} as any) + ]) - const ySame = new Date(now.getTime() - 24 * 60 * 60 * 1000) - const y0 = new Date(ySame.getFullYear(), ySame.getMonth(), ySame.getDate()) + const obj: any = kpiRow != null ? kpiRow : ({} as any) - 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) + gmv: Math.round(safeNumber(obj.getAny?.('gmv') ?? 0)), + gmv_growth: safeNumber(obj.getAny?.('gmv_growth') ?? 0), + orders: Math.round(safeNumber(obj.getAny?.('orders') ?? 0)), + order_growth: safeNumber(obj.getAny?.('order_growth') ?? 0), + online_users: Math.round(safeNumber(onlineUsersVal ?? 0)), + conversion_rate: safeNumber(obj.getAny?.('conversion_rate') ?? 0), + conversion_growth: safeNumber(obj.getAny?.('conversion_growth') ?? 0) } } @@ -75,8 +59,7 @@ export async function fetchDashboardTopProducts(period: string, limit: number = 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 + p_limit: limit } as any) const list: Array = [] @@ -86,7 +69,7 @@ export async function fetchDashboardTopProducts(period: string, limit: number = id: `${row.getAny?.('id') ?? i}`, rank: i + 1, name: `${row.getAny?.('name') ?? '未知商品'}`, - sales: safeNumber(row.getAny?.('sales') ?? row.getAny?.('total_amount') ?? 0) + sales: safeNumber(row.getAny?.('sales') ?? 0) }) } return list @@ -107,8 +90,8 @@ export async function fetchDashboardTopMerchants(period: string, limit: number = 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) + sales: safeNumber(row.getAny?.('sales') ?? 0), + growth: safeNumber(row.getAny?.('growth') ?? 0) }) } return list diff --git a/services/analytics/dataDetailService.uts b/services/analytics/dataDetailService.uts index 579c4fe9..6a3065cf 100644 --- a/services/analytics/dataDetailService.uts +++ b/services/analytics/dataDetailService.uts @@ -1,70 +1,58 @@ import { rpcOrEmptyArray, rpcOrNull } from './rpc.uts' -export type DataDetailReportInfo = { - period: string -} - -export type DataDetailRow = { +export type ReportInfo = { id: string - date: string - gmv: number - orders: number - users: number + title: string + description: string + definition: any + updated_at: string } -export type DataDetailDrillItem = { - id: string - label: string - value: string - type: string +function safeString(v: any): string { + return v != null ? `${v}` : '' } -export async function fetchDataDetailReportInfo(reportId: string): Promise { - const info = await rpcOrNull('rpc_data_detail_report_info', { +// 改造:调用 rpc_data_detail_report_info +export async function fetchReportInfo(reportId: string): Promise { + const row = await rpcOrNull('rpc_data_detail_report_info', { p_report_id: reportId - } as UTSJSONObject) - if (info == null) return null - return { period: info.getString('period') ?? '' } + } as any) + + if (row == null) return null + + return { + id: safeString(row.getAny?.('id')), + title: safeString(row.getAny?.('title')), + description: safeString(row.getAny?.('description')), + definition: row.getAny?.('definition'), + updated_at: safeString(row.getAny?.('updated_at')) + } } -export async function fetchDataDetailRows(reportId: string, sortBy: string, sortDir: string, limit: number, offset: number): Promise> { - const rows = await rpcOrEmptyArray('rpc_data_detail_rows', { +// 改造:调用 rpc_data_detail_rows +export async function fetchReportRows(reportId: string, params: any): Promise> { + const result = await rpcOrNull('rpc_data_detail_rows', { p_report_id: reportId, - p_sort_by: sortBy, - p_sort_dir: sortDir, - p_limit: limit, - p_offset: offset - } as UTSJSONObject) + p_params: params + } as any) - const out: Array = [] - 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 + if (result == null) return [] + const anyData = result as any + return Array.isArray(anyData) ? (anyData as Array) : ([] as Array) } -export async function fetchDataDetailDrillItems(reportId: string): Promise> { - const rows = await rpcOrEmptyArray('rpc_data_detail_drill_items', { - p_report_id: reportId - } as UTSJSONObject) - - const out: Array = [] - 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 +// 保留调用,但 RPC 是模拟数据 +export async function fetchDrilldown(reportId: string, itemId: string): Promise> { + return await rpcOrEmptyArray('rpc_data_detail_drill_items', { + p_report_id: reportId, + p_item_id: itemId + } as any) +} + +// 保留调用,但 RPC 是模拟数据 +export async function fetchComparison(itemId: string, period: string): Promise> { + return await rpcOrEmptyArray('rpc_data_detail_compare_gmv', { + p_item_id: itemId, + p_period: period + } as any) } diff --git a/services/analytics/deliveryAnalysisService.uts b/services/analytics/deliveryAnalysisService.uts index 548c7adc..4e4f9fe5 100644 --- a/services/analytics/deliveryAnalysisService.uts +++ b/services/analytics/deliveryAnalysisService.uts @@ -1,5 +1,5 @@ -import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts' import { computeDateRange } from './dateRange.uts' +import { rpcOrEmptyArray } from './rpc.uts' export type DeliveryAnalysisData = { trendList: Array @@ -11,122 +11,16 @@ export type DeliveryAnalysisData = { export async function fetchDeliveryAnalysis(period: string): Promise { const { startIso, endIso } = computeDateRange(period) - await ensureSupabaseReady() - - // 优先走 RPC(需要在 Supabase 执行 DELIVERY_ANALYSIS_RPCS.sql 创建函数) - let trendList: Array = [] - let topList: Array = [] - - const trendRes: any = await supa.rpc('rpc_delivery_efficiency_daily', { + const trendList = await rpcOrEmptyArray('rpc_delivery_efficiency_daily', { p_start: startIso, p_end: endIso - } as UTSJSONObject) + } as any) - 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) : [] - - const dayAgg = new Map() - const driverAgg = new Map() - const driverFeeAgg = new Map() - const driverTimeAgg = new Map() - - 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) : [] - - // 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) : [] - } - } + const topList = await rpcOrEmptyArray('rpc_delivery_efficiency_top_drivers', { + p_start: startIso, + p_end: endIso, + p_limit: 10 + } as any) return { trendList, topList, startIso, endIso } } diff --git a/services/analytics/productInsightsService.uts b/services/analytics/productInsightsService.uts index 29bdbeaf..34c13b19 100644 --- a/services/analytics/productInsightsService.uts +++ b/services/analytics/productInsightsService.uts @@ -23,8 +23,8 @@ function safeNumber(v: any): number { export async function fetchProductOverview(period: string): Promise { const { startIso, endIso } = computeDateRange(period) const row = await rpcOrNull('rpc_product_insights_overview', { - p_start: startIso, - p_end: endIso + p_start: toDateOnly(startIso), + p_end: toDateOnly(endIso) } as any) const obj: any = row != null ? row : ({} as any) @@ -44,8 +44,7 @@ export async function fetchTopProducts(period: string, limit: number = 10): Prom 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 + p_limit: limit } as any) const list: Array = [] @@ -55,8 +54,8 @@ export async function fetchTopProducts(period: string, limit: number = 10): Prom 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) + sales: safeNumber(r.getAny?.('sales') ?? 0), + growth: safeNumber(r.getAny?.('growth') ?? 0) }) } return list @@ -73,12 +72,12 @@ export async function fetchProductTrend(period: string, productId: string): Prom const out: Array = [] 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') ?? ''}` + const date = `${r.getAny?.('date') ?? ''}` 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) + gmv: safeNumber(r.getAny?.('gmv') ?? 0), + qty: safeNumber(r.getAny?.('qty') ?? 0), + orders: safeNumber(r.getAny?.('orders') ?? 0) }) } return out @@ -93,11 +92,7 @@ export async function fetchCategorySales(period: string): Promise> { - const { startIso, endIso } = computeDateRange(period) - return await rpcOrEmptyArray('rpc_product_insights_stock', { - p_start: startIso, - p_end: endIso - } as any) + return await rpcOrEmptyArray('rpc_product_insights_stock', {} as any) } export async function fetchPriceTrend(period: string): Promise> { diff --git a/services/analytics/salesReportService.uts b/services/analytics/salesReportService.uts index c14f3a21..b8749f1b 100644 --- a/services/analytics/salesReportService.uts +++ b/services/analytics/salesReportService.uts @@ -24,22 +24,9 @@ function safeNumber(v: any): number { export async function fetchSalesKpis(period: string): Promise { 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 + const row = await rpcOrNull('rpc_analytics_sales_kpis', { + p_start_date: toDateOnly(startIso), + p_end_date: toDateOnly(endIso) } as any) const obj: any = row != null ? row : ({} as any) @@ -61,10 +48,9 @@ export async function fetchSalesKpis(period: string): Promise { export async function fetchSalesTrend(period: string): Promise { const { startIso, endIso } = computeDateRange(period) - const rows = await rpcOrEmptyArray('rpc_analytics_trend_data', { + const rows = await rpcOrEmptyArray('rpc_analytics_sales_trend', { p_start_date: toDateOnly(startIso), - p_end_date: toDateOnly(endIso), - p_merchant_id: null + p_end_date: toDateOnly(endIso) } as any) const x: Array = [] @@ -87,8 +73,7 @@ export async function fetchSalesTopProducts(period: string, limit: number = 50): 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 + p_limit: limit } as any) const list: Array = [] diff --git a/types/analytics.uts b/types/analytics.uts new file mode 100644 index 00000000..b3891595 --- /dev/null +++ b/types/analytics.uts @@ -0,0 +1,3 @@ +// types/analytics.uts + +export type AnalyticsTypesMigrated = true diff --git a/types/analytics/common.uts b/types/analytics/common.uts new file mode 100644 index 00000000..d6e186f6 --- /dev/null +++ b/types/analytics/common.uts @@ -0,0 +1,5 @@ +// types/analytics/common.uts + +export type TimePeriod = { value: string; label: string } +export type ChartType = { value: string; label: string } +export type Metric = { key: string; label: string } diff --git a/types/analytics/coupon.uts b/types/analytics/coupon.uts new file mode 100644 index 00000000..985cab5d --- /dev/null +++ b/types/analytics/coupon.uts @@ -0,0 +1,11 @@ +// types/analytics/coupon.uts + +export type CouponData = { + total_issued: number + issued_growth: number + total_used: number + usage_rate: number + gmv_increase: number + gmv_growth: number + roi: number +} diff --git a/types/analytics/custom-report.uts b/types/analytics/custom-report.uts new file mode 100644 index 00000000..c52f4298 --- /dev/null +++ b/types/analytics/custom-report.uts @@ -0,0 +1,29 @@ +// types/analytics/custom-report.uts + +import type { Metric, TimePeriod, ChartType } from './common.uts' + +export type CustomReport = { + id: string + name: string + description: string + metrics: Array + charts: Array + updated_at: string + period?: string +} + +export type ReportForm = { + name: string + description: string + metrics: Array + period: string + chartType: string +} + +export type ReportFormErrors = { + name: string + description: string + metrics: string + period: string + chartType: string +} diff --git a/types/analytics/dashboard.uts b/types/analytics/dashboard.uts new file mode 100644 index 00000000..f73c7c96 --- /dev/null +++ b/types/analytics/dashboard.uts @@ -0,0 +1,7 @@ +// types/analytics/dashboard.uts + +export type TrendData = { x: Array; gmv: Array; orders: Array } +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 } diff --git a/types/analytics/data-detail.uts b/types/analytics/data-detail.uts new file mode 100644 index 00000000..76d09564 --- /dev/null +++ b/types/analytics/data-detail.uts @@ -0,0 +1,4 @@ +// types/analytics/data-detail.uts + +export type TableColumn = { key: string; label: string; type: string; sortable: boolean } +export type DrillDownItem = { id: string; label: string; value: string; type: string } diff --git a/types/analytics/delivery.uts b/types/analytics/delivery.uts new file mode 100644 index 00000000..203451dd --- /dev/null +++ b/types/analytics/delivery.uts @@ -0,0 +1,25 @@ +// types/analytics/delivery.uts + +/** + * Key Performance Indicators for the Delivery Analysis page. + */ +export type DeliveryData = { + avg_delivery_time: number; + time_growth: number; + total_fee: number; + avg_fee: number; + avg_orders_per_driver: number; + satisfaction_rate: number; + satisfaction_growth: number; +}; + +/** + * Represents a driver's ranking based on performance. + */ +export type DriverRank = { + id: string; + rank: number; + name: string; + orders: number; + rating: number +}; diff --git a/types/analytics/insight.uts b/types/analytics/insight.uts new file mode 100644 index 00000000..f86175a3 --- /dev/null +++ b/types/analytics/insight.uts @@ -0,0 +1,19 @@ +// types/analytics/insight.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 +} diff --git a/types/analytics/market.uts b/types/analytics/market.uts new file mode 100644 index 00000000..8ea60a5c --- /dev/null +++ b/types/analytics/market.uts @@ -0,0 +1,9 @@ +// types/analytics/market.uts + +export type MarketTrendsResponse = { + trendRows: any + categoryRows: any + seasonalRows: any + priceRows: any + competitionRows: any +} diff --git a/types/analytics/product.uts b/types/analytics/product.uts new file mode 100644 index 00000000..46286048 --- /dev/null +++ b/types/analytics/product.uts @@ -0,0 +1,14 @@ +// types/analytics/product.uts + +export type ProductData = { + 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 } diff --git a/types/analytics/profile.uts b/types/analytics/profile.uts new file mode 100644 index 00000000..610d0221 --- /dev/null +++ b/types/analytics/profile.uts @@ -0,0 +1,38 @@ +// types/analytics/profile.uts + +export type ReportStatus = 'pending' | 'ready' | 'failed' | 'scheduled' | 'shared' | string + +export type RecentReport = { + id: string + title: string + description: string + status: ReportStatus + created_at: string +} + +export type OverviewData = { + totalSales: string + salesGrowth: number + totalUsers: string + userGrowth: number + totalOrders: string + orderGrowth: number + conversionRate: number + conversionGrowth: number +} + +export type ReportCounts = { + total: number + pending: number + scheduled: number + shared: number +} + +export type TodayInsights = { + hotProduct: string + peakTraffic: string + conversionAnomaly: string + mobileRatio: number +} + +export type TrendDatum = { label: string; sales: number; orders: number } diff --git a/types/analytics/report-detail.uts b/types/analytics/report-detail.uts new file mode 100644 index 00000000..3cdc1786 --- /dev/null +++ b/types/analytics/report-detail.uts @@ -0,0 +1,46 @@ +// types/analytics/report-detail.uts + +export type ReportType = { + id: string; + title: string; + type: string; + period: string; + generated_at: string; + description: string; +}; + +export type MetricType = { + key: string; + label: string; + value: number; + format: string; + icon: string; + color: string; + change: number; +}; + +export type ChartTabType = { + key: string; + label: string; +}; + +export type ChartLegendType = { + key: string; + label: string; + color: string; +}; + +export type TableColumnType = { + key: string; + title: string; + width: string; + type: string; +}; + +export type InsightType = { + id: string; + type: string; + title: string; + content: string; + impact: string; +}; diff --git a/types/analytics/sales.uts b/types/analytics/sales.uts new file mode 100644 index 00000000..55e4757b --- /dev/null +++ b/types/analytics/sales.uts @@ -0,0 +1,22 @@ +// types/analytics/sales.uts + +// Re-exporting shared types from dashboard for semantic clarity in the sales context. +import type { TrendData, TopProductItem, TopMerchantItem } from './dashboard.uts' + +export type SalesTrendData = TrendData +export type ProductRank = TopProductItem +export type MerchantRank = TopMerchantItem + +/** + * Key Performance Indicators for the Sales Report page. + */ +export type SalesData = { + gmv: number + gmv_growth: number + orders: number + order_growth: number + conversion_rate: number + conversion_growth: number + avg_order_amount: number + avg_order_growth: number +} diff --git a/types/analytics/user.uts b/types/analytics/user.uts new file mode 100644 index 00000000..216404cb --- /dev/null +++ b/types/analytics/user.uts @@ -0,0 +1,25 @@ +// types/analytics/user.uts + +/** + * Key Performance Indicators for the User Analysis page. + */ +export type UserData = { + total_users: number; + user_growth: number; + new_users: number; + new_user_growth: number; + active_users: number; + active_growth: number; + ordering_users: number; + ordering_growth: number; + paid_users: number; + paid_growth: number; + new_user_conversion_rate: number; + repurchase_rate: number; + repurchase_growth: number; +} + +/** + * Represents a single step in a conversion funnel. + */ +export type FunnelStep = { step: string; value: number }; From 19970db2889644bce3834839d06d039e8905a4a1 Mon Sep 17 00:00:00 2001 From: comlibmb <1844410276@qq.com> Date: Sun, 1 Feb 2026 20:17:37 +0800 Subject: [PATCH 13/18] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=86=E6=9E=90ui?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=AE=8C=E5=96=84=EF=BC=8C=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ak/config.uts | 6 +- .../analytics/AnalyticsDateRangePicker.uvue | 129 +++++++ pages.json | 6 - pages/mall/analytics/coupon-analysis.uvue | 3 +- pages/mall/analytics/custom-report.uvue | 359 +----------------- pages/mall/analytics/data-detail.uvue | 3 +- pages/mall/analytics/delivery-analysis.uvue | 3 +- pages/mall/analytics/index.uvue | 76 +++- pages/mall/analytics/insight-detail.uvue | 3 +- pages/mall/analytics/market-trends.uvue | 3 +- pages/mall/analytics/product-insights.uvue | 3 +- pages/mall/analytics/profile.uvue | 16 +- pages/mall/analytics/report-detail.uvue | 25 +- pages/mall/analytics/sales-report.uvue | 57 ++- pages/mall/analytics/user-analysis.uvue | 11 +- services/analytics/dashboardService.uts | 15 +- services/analytics/salesReportService.uts | 56 ++- utils/sapi.uts | 28 +- utils/utils.uts | 26 ++ 19 files changed, 393 insertions(+), 435 deletions(-) create mode 100644 components/analytics/AnalyticsDateRangePicker.uvue diff --git a/ak/config.uts b/ak/config.uts index e235d9a1..b9ac3206 100644 --- a/ak/config.uts +++ b/ak/config.uts @@ -2,11 +2,11 @@ // 内网环境 - 本地部署的 Supabase // IP: 192.168.1.63 // Kong HTTP Port: 8000 -export const SUPA_URL: string = 'http://192.168.1.63:8000' -export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY4ODMwNjI0LCJleHAiOjE5MjY1MTA2MjR9.mDVl-kIOdRK9v6VTxo0TDF8r7X7xk3PZXazaavHyVvg' +export const SUPA_URL: string = 'http://192.168.1.63:18000' +export const SUPA_KEY: string = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJyb2xlIjogImFub24iLCAiaXNzIjogInN1cGFiYXNlIiwgImlhdCI6IDE3Njk4NDczMzQsICJleHAiOiAyMDg1MjA3MzM0fQ.js-2CS5_cUmf4iVv8aCmmx9iyFsQvLNDbt8YYOngeLU' // WebSocket 实时连接(内网使用 ws:// 而非 wss://) -export const WS_URL: string = 'ws://192.168.1.63:8000/realtime/v1/websocket' +export const WS_URL: string = 'ws://192.168.1.63:18000/realtime/v1/websocket' // 备用配置(已注释,如需切换可取消注释) // 开发环境 - 其他内网地址 diff --git a/components/analytics/AnalyticsDateRangePicker.uvue b/components/analytics/AnalyticsDateRangePicker.uvue new file mode 100644 index 00000000..57e5e91c --- /dev/null +++ b/components/analytics/AnalyticsDateRangePicker.uvue @@ -0,0 +1,129 @@ + + + + + diff --git a/pages.json b/pages.json index 314253fb..4a62865b 100644 --- a/pages.json +++ b/pages.json @@ -411,12 +411,6 @@ "navigationBarTitleText": "数据洞察详情", "enablePullDownRefresh": false } - }, - { - "path": "test/test-connection", - "style": { - "navigationBarTitleText": "Supabase 连接测试" - } } ] }, diff --git a/pages/mall/analytics/coupon-analysis.uvue b/pages/mall/analytics/coupon-analysis.uvue index 8eddf244..11b03e5e 100644 --- a/pages/mall/analytics/coupon-analysis.uvue +++ b/pages/mall/analytics/coupon-analysis.uvue @@ -109,7 +109,8 @@ diff --git a/pages/mall/consumer/category.uvue b/pages/mall/consumer/category.uvue index ec734cdf..a26780fb 100644 --- a/pages/mall/consumer/category.uvue +++ b/pages/mall/consumer/category.uvue @@ -122,6 +122,7 @@ const productList = ref([]) const activePrimary = ref('') const cartCount = ref(3) const hasMore = ref(true) +const hasLoadedFromParams = ref(false) // 标记是否已通过参数加载 // 获取当前分类信息 const currentCategoryName = ref('') @@ -134,34 +135,67 @@ const pageParams = ref({}) // 生命周期 onMounted(async() => { await loadCategories() - await loadProducts() + // 等待分类加载完成后,再检查是否需要加载默认分类的商品 + // 延迟一点时间,确保页面参数处理完成 + setTimeout(async () => { + if (!hasLoadedFromParams.value && activePrimary.value) { + await loadProducts() + } + }, 300) }) // 添加加载分类的方法 const loadCategories = async () => { - const categories = await supabaseService.getCategories() - if (categories.length > 0) { - primaryCategories.value = categories - // 设置默认选中第一个分类 - if (!activePrimary.value && categories[0]) { - activePrimary.value = categories[0].id + try { + const categories = await supabaseService.getCategories() + console.log('加载分类数据成功,数量:', categories.length) + if (categories.length > 0) { + primaryCategories.value = categories + // 如果没有通过参数设置分类,则设置默认选中第一个分类 + if (!activePrimary.value && categories[0]) { + activePrimary.value = categories[0].id + console.log('设置默认分类为:', categories[0].name, 'ID:', categories[0].id) + } + } else { + console.warn('从Supabase获取的分类数据为空') } + } catch (error) { + console.error('加载分类数据失败:', error) } } // 加载商品数据 const loadProducts = async () => { - if (activePrimary.value) { - const response = await supabaseService.getProductsByCategory(activePrimary.value) - productList.value = response.data - hasMore.value = response.hasmore - - // 更新当前分类信息 - const category = primaryCategories.value.find(cat => cat.id === activePrimary.value) - if (category) { - currentCategoryName.value = category.name - currentCategoryDesc.value = category.description + try { + if (activePrimary.value) { + console.log('开始加载商品,分类ID:', activePrimary.value) + const response = await supabaseService.getProductsByCategory(activePrimary.value) + console.log('商品加载结果:', { + dataCount: response.data.length, + total: response.total, + hasmore: response.hasmore + }) + + productList.value = response.data + hasMore.value = response.hasmore + + // 更新当前分类信息 + const category = primaryCategories.value.find(cat => cat.id === activePrimary.value) + if (category) { + currentCategoryName.value = category.name + currentCategoryDesc.value = category.description || '' + console.log('当前分类信息:', category.name, '描述:', category.description) + } else { + console.warn('未找到对应的分类信息,分类ID:', activePrimary.value) + } + + console.log('商品列表加载完成,数量:', productList.value.length) + } else { + console.warn('activePrimary为空,无法加载商品') } + } catch (error) { + console.error('加载商品数据失败:', error) + productList.value = [] } } @@ -200,6 +234,7 @@ onLoad((options: any) => { // 如果有找到分类ID,则选中对应的分类 if (categoryId) { + hasLoadedFromParams.value = true console.log('✅ 准备选中分类:', categoryId) console.log('分类名称:', categoryName || '未指定') @@ -244,6 +279,7 @@ onShow(() => { // 检查是否有分类参数 if (pageOptions.categoryId) { + hasLoadedFromParams.value = true const categoryId = pageOptions.categoryId const categoryName = pageOptions.name || '' @@ -288,6 +324,7 @@ onShow(() => { const params = new URLSearchParams(queryString) const urlCategoryId = params.get('categoryId') if (urlCategoryId) { + hasLoadedFromParams.value = true console.log('✅ 从URL解析到分类参数:', urlCategoryId) selectPrimaryCategory(urlCategoryId) } diff --git a/pages/mall/consumer/category药品.uvue b/pages/mall/consumer/category药品.uvue new file mode 100644 index 00000000..ec734cdf --- /dev/null +++ b/pages/mall/consumer/category药品.uvue @@ -0,0 +1,1131 @@ + + + + + diff --git a/pages/mall/consumer/checkout.uvue b/pages/mall/consumer/checkout.uvue index d96e6e2a..f68dfd6f 100644 --- a/pages/mall/consumer/checkout.uvue +++ b/pages/mall/consumer/checkout.uvue @@ -772,64 +772,10 @@ const loadDefaultAddress = async () => { // 获取当前用户ID const getCurrentUserId = (): string => { - // 尝试从多个可能的键名获取用户ID - const possibleKeys = ['user_id', 'userId', 'uid', 'user_uuid', 'userID', 'user.id'] - - for (const key of possibleKeys) { - const value = uni.getStorageSync(key) - console.log(`getCurrentUserId: 尝试键名 ${key}:`, value) - if (value) { - console.log(`getCurrentUserId: 从 ${key} 获取到用户ID:`, value) - return value as string - } - } - - // 尝试从userInfo对象获取 - const userInfo = uni.getStorageSync('userInfo') - console.log('getCurrentUserId: 从userInfo获取:', userInfo) - if (userInfo) { - // userInfo可能是字符串(需要解析)或对象 - let userInfoObj: any = userInfo - if (typeof userInfo === 'string') { - try { - userInfoObj = JSON.parse(userInfo) - } catch (e) { - console.error('解析userInfo失败:', e) - } - } - - // 尝试多个可能的属性名 - const possibleProps = ['id', 'userId', 'uid', 'user_id', 'uuid', 'user_uuid'] - for (const prop of possibleProps) { - if (userInfoObj && userInfoObj[prop]) { - console.log(`getCurrentUserId: 从userInfo.${prop} 获取到用户ID:`, userInfoObj[prop]) - return userInfoObj[prop] as string - } - } - } - - // 尝试从auth获取(如果使用Supabase Auth) - const authData = uni.getStorageSync('supabase.auth.token') - if (authData) { - console.log('getCurrentUserId: 从supabase.auth.token获取:', authData) - try { - const authObj = typeof authData === 'string' ? JSON.parse(authData) : authData - if (authObj.currentSession && authObj.currentSession.user && authObj.currentSession.user.id) { - console.log('getCurrentUserId: 从auth session获取用户ID:', authObj.currentSession.user.id) - return authObj.currentSession.user.id as string - } - } catch (e) { - console.error('解析auth数据失败:', e) - } - } - - // 打印所有存储键,用于调试 - console.log('getCurrentUserId: 所有Storage键:') - const allKeys = uni.getStorageInfoSync().keys - console.log('Storage keys:', allKeys) - - console.log('getCurrentUserId: 未找到用户ID') - return '' + // 使用 SupabaseService 获取当前用户ID + const userId = supabaseService.getCurrentUserId() + console.log('getCurrentUserId: 从SupabaseService获取到用户ID:', userId) + return userId ?? '' } // 用户登录状态 @@ -1280,65 +1226,89 @@ const selectCoupon = () => { // 提交订单 const submitOrder = async () => { - if (!selectedAddress.value) { - uni.showToast({ - title: '请选择收货地址', - icon: 'none' - }) - return - } + // 校验地址 + if (!selectedAddress.value) { + uni.showToast({ + title: '请选择收货地址', + icon: 'none' + }) + return + } + + // 校验商品 + if (checkoutItems.value.length === 0) { + uni.showToast({ + title: '订单中没有商品', + icon: 'none' + }) + return + } + + uni.showLoading({ title: '提交中...' }) - // MOCK ORDER SUBMISSION - // 模拟创建成功 try { - const mockOrderId = `order_${Date.now()}` + const userId = getCurrentUserId() + // 确保使用当前登录用户ID (如果本地存储为空,可能需要处理) + if (!userId) { + uni.hideLoading() + uni.showToast({ + title: '请先登录', + icon: 'none' + }) + return + } - // 创建订单对象 - const newOrder = { - id: mockOrderId, - order_no: generateOrderNo(), - user_id: getCurrentUserId() || 'user_001', - merchant_id: checkoutItems.value[0]?.product_id || 'merchant_001', // 简化处理,取第一个商品的merchant - status: 1, // 待支付 - total_amount: totalAmount.value, - discount_amount: discountAmount.value, - delivery_fee: deliveryFee.value, - actual_amount: actualAmount.value, - payment_method: 0, - payment_status: 0, - delivery_address: selectedAddress.value, - items: checkoutItems.value, - created_at: new Date().toISOString() - } + // 准备订单项数据 + // 注意:需根据 checkoutItems 的实际结构转换为 createOrder 需要的 CartItem 结构 + // 假设 checkoutItems 已经包含了 product_id, quantity, price, name, image 等字段 + const orderItems = checkoutItems.value.map((item: any): any => ({ + id: item.id || '', // 这是一个临时ID或者购物车ID,createOrder 中会使用 product_id + product_id: item.product_id || item.id, // 确保有 product_id + quantity: item.quantity, + price: item.price, + product_name: item.name, + product_image: item.image, + spec: item.spec, + checked: true + })) - // 保存到本地存储 - const storedOrders = uni.getStorageSync('orders') - let orders: any[] = [] - if (storedOrders) { - try { - orders = JSON.parse(storedOrders as string) as any[] - } catch (e) { - console.error('解析订单数据失败', e) - } - } - orders.unshift(newOrder) - uni.setStorageSync('orders', JSON.stringify(orders)) - - uni.showLoading({ title: '提交中...' }) - await new Promise(resolve => setTimeout(resolve, 500)) + // 调用 Supabase 服务创建订单 + const result = await supabaseService.createOrder( + userId, + selectedAddress.value!.id, // 地址ID + actualAmount.value, // 实付金额 + orderItems + ) + uni.hideLoading() - - // 携带价格详情跳转 - uni.navigateTo({ - url: `/pages/mall/consumer/payment?orderId=${mockOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}` - }) - } catch (err) { - console.error('创建订单失败:', err) - uni.showToast({ - title: '订单创建失败', - icon: 'none' - }) - } + + if (result.success) { + // 清除购买的商品 (如果来自购物车,应该在 createOrder 成功后清除,或者这里手动清除本地存储) + // 这里我们假设购物车清理逻辑可能在 createOrder 后端处理,或者需要在这里清除本地 + try { + uni.removeStorageSync('checkout_items') + } catch(e) { + console.error('清除结算商品失败', e) + } + + const activeOrderId = result.data as string + + // 跳转支付页面 + uni.navigateTo({ + url: `/pages/mall/consumer/payment?orderId=${activeOrderId}&amount=${actualAmount.value}&productAmount=${totalAmount.value}&deliveryFee=${deliveryFee.value}&discountAmount=${discountAmount.value}` + }) + } else { + throw new Error(result.error) + } + + } catch (err: any) { + uni.hideLoading() + console.error('创建订单失败:', err) + uni.showToast({ + title: err.message || '订单创建失败', + icon: 'none' + }) + } } // 生成订单号 diff --git a/pages/mall/consumer/favorites.uvue b/pages/mall/consumer/favorites.uvue index 693a201a..ad1c8d8a 100644 --- a/pages/mall/consumer/favorites.uvue +++ b/pages/mall/consumer/favorites.uvue @@ -31,6 +31,7 @@ + + diff --git a/pages/mall/consumer/orders.uvue b/pages/mall/consumer/orders.uvue index f33fd102..19de501a 100644 --- a/pages/mall/consumer/orders.uvue +++ b/pages/mall/consumer/orders.uvue @@ -156,9 +156,11 @@ - - diff --git a/pages/mall/consumer/product-detail.uvue b/pages/mall/consumer/product-detail.uvue index a1da08fa..9e240061 100644 --- a/pages/mall/consumer/product-detail.uvue +++ b/pages/mall/consumer/product-detail.uvue @@ -373,7 +373,18 @@ export default { // 尝试多种方式访问属性 const idValue = dbProduct.id !== undefined ? dbProduct.id : (dbProduct['id'] !== undefined ? dbProduct['id'] : undefined) const nameValue = dbProduct.name !== undefined ? dbProduct.name : (dbProduct['name'] !== undefined ? dbProduct['name'] : undefined) - const priceValue = dbProduct.price !== undefined ? dbProduct.price : (dbProduct['price'] !== undefined ? dbProduct['price'] : undefined) + + // 价格字段兼容性处理:优先查找 price,其次查找 base_price + let priceValue = dbProduct.price + if (priceValue === undefined || priceValue === null) { + priceValue = dbProduct.base_price + } + if (priceValue === undefined || priceValue === null) { + priceValue = dbProduct['price'] + } + if (priceValue === undefined || priceValue === null) { + priceValue = dbProduct['base_price'] + } const hasId = idValue !== undefined && idValue !== null const hasName = nameValue !== undefined && nameValue !== null @@ -396,33 +407,27 @@ export default { // 数据库Product接口和本地ProductType接口字段可能不同 const images = [] as Array - // 处理图片字段:优先使用images字段,其次使用image字段 - console.log('处理数据库图片字段:') - console.log('dbProduct.images:', dbProduct.images, '类型:', typeof dbProduct.images) - console.log('dbProduct.image:', dbProduct.image, '类型:', typeof dbProduct.image) + // 处理图片字段:优先使用image_urls字段,其次使用main_image_url + console.log('处理数据库图片字段') - // 尝试从数据库的images字段获取图片(可能是字符串或数组) - if (dbProduct.images) { + // 尝试从数据库的image_urls字段获取图片(JSON字符串或对象) + if (dbProduct.image_urls) { let imagesArray: any[] = [] - if (typeof dbProduct.images === 'string') { + if (typeof dbProduct.image_urls === 'string') { try { - imagesArray = JSON.parse(dbProduct.images) - console.log('解析images字符串成功:', imagesArray) + imagesArray = JSON.parse(dbProduct.image_urls) } catch (e) { - console.error('解析images字段失败:', e, dbProduct.images) - // 如果不是JSON,尝试按逗号分割 - if (dbProduct.images.includes(',')) { - imagesArray = dbProduct.images.split(',').map((img: string) => img.trim()) - } else if (dbProduct.images) { - imagesArray = [dbProduct.images] + console.error('解析image_urls字段失败:', e, dbProduct.image_urls) + // 尝试逗号分割 + if (dbProduct.image_urls.includes(',')) { + imagesArray = dbProduct.image_urls.split(',').map((img: string) => img.trim()) } } - } else if (Array.isArray(dbProduct.images)) { - imagesArray = dbProduct.images + } else if (Array.isArray(dbProduct.image_urls)) { + imagesArray = dbProduct.image_urls } if (imagesArray.length > 0) { - console.log('从数据库images字段获取图片数组:', imagesArray) for (const img of imagesArray) { if (typeof img === 'string' && img) { images.push(img) @@ -430,11 +435,18 @@ export default { } } } - - // 如果没有从images字段获取到图片,尝试使用image字段 + + // 如果没有获取到相册图,但有主图,放入相册 + if (dbProduct.main_image_url) { + // 如果相册里没有这张图,把它加到第一位 + if (!images.includes(dbProduct.main_image_url)) { + images.unshift(dbProduct.main_image_url) + } + } + + // 兼容旧字段 image if (images.length === 0 && dbProduct.image) { - console.log('使用单张图片字段:', dbProduct.image) - images.push(dbProduct.image) + images.push(dbProduct.image) } // 如果仍然没有图片,使用传入的图片或默认图片 @@ -461,9 +473,33 @@ export default { const merchantId = dbProduct.shop_id || dbProduct.merchant_id || 'merchant_001' // 确保数值字段有效 - const price = typeof dbProduct.price === 'number' ? dbProduct.price : 0 - const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : 100 - const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : 50 + // 优先使用 price,不存在则使用 base_price + let productPrice = 0 + if (typeof dbProduct.price === 'number') { + productPrice = dbProduct.price + } else if (typeof dbProduct.base_price === 'number') { + productPrice = dbProduct.base_price + } else if (priceValue !== undefined) { + // 使用上面校验时获取到的 priceValue + productPrice = Number(priceValue) + } + + const stock = (dbProduct.stock != null && !isNaN(Number(dbProduct.stock))) ? Math.floor(Number(dbProduct.stock)) : ((dbProduct.total_stock != null && !isNaN(Number(dbProduct.total_stock))) ? Math.floor(Number(dbProduct.total_stock)) : 100) + const sales = (dbProduct.sales != null && !isNaN(Number(dbProduct.sales))) ? Math.floor(Number(dbProduct.sales)) : ((dbProduct.sale_count != null && !isNaN(Number(dbProduct.sale_count))) ? Math.floor(Number(dbProduct.sale_count)) : 50) + + // 解析 attributes + let attributes: any = {} + if (dbProduct.attributes) { + try { + if (typeof dbProduct.attributes === 'string') { + attributes = JSON.parse(dbProduct.attributes) + } else { + attributes = dbProduct.attributes + } + } catch (e) { + console.error('解析 attributes 失败', e) + } + } this.product = { id: dbProduct.id || productId, @@ -472,20 +508,20 @@ export default { name: dbProduct.name || '商品名称', description: dbProduct.description || '这是一个高品质的商品,具有优秀的性能和优美的外观设计。采用环保材料,经过严格质检,保证用户的使用体验。', images: images, - price: price, - original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : null, + price: productPrice, + original_price: (dbProduct.original_price != null && !isNaN(Number(dbProduct.original_price))) ? Number(dbProduct.original_price) : ((dbProduct.market_price != null && !isNaN(Number(dbProduct.market_price))) ? Number(dbProduct.market_price) : null), stock: stock, sales: sales, status: 1, created_at: dbProduct.created_at || '2024-01-01', // 药品相关字段 - specification: dbProduct.specification || null, - usage: dbProduct.usage || null, - side_effects: dbProduct.side_effects || null, - precautions: dbProduct.precautions || null, - expiry_date: dbProduct.expiry_date || null, - storage_conditions: dbProduct.storage_conditions || null, - approval_number: dbProduct.approval_number || null, + specification: attributes.specification || dbProduct.specification || null, + usage: attributes.usage || dbProduct.usage || null, + side_effects: attributes.side_effects || dbProduct.side_effects || null, + precautions: attributes.precautions || dbProduct.precautions || null, + expiry_date: attributes.expiry_date || dbProduct.expiry_date || null, + storage_conditions: attributes.storage_conditions || dbProduct.storage_conditions || null, + approval_number: attributes.approval_number || dbProduct.approval_number || null, tags: dbProduct.tags ? (typeof dbProduct.tags === 'string' ? JSON.parse(dbProduct.tags) : dbProduct.tags) : [] } as ProductType console.log('页面 product 对象已更新:', this.product) @@ -535,37 +571,109 @@ export default { } } - // 根据商家ID生成不同的商家信息 - const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 - const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店'] - const shopDescriptions = [ - '专注品质生活', - '品牌官方直营,正品保障', - '厂家直销,价格优惠', - '专注本领域十年老店', - '用心服务每一位顾客' - ] - const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱'] - - this.merchant = { - id: this.product.merchant_id, - user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'), - shop_name: shopNames[merchantIndex], - shop_logo: '/static/shop-logo.png', - shop_banner: '/static/shop-banner.png', - shop_description: shopDescriptions[merchantIndex], - contact_name: contactNames[merchantIndex], - contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8), - shop_status: 1, - rating: 4.5 + (merchantIndex * 0.1), - total_sales: 10000 + merchantIndex * 5000, - created_at: '2023-06-01' + // 尝试加载真实商户信息 + let realMerchantLoaded = false + // 只有当 ID 是 UUID 格式(包含-)或者是真实数据时才尝试查询 + if (this.product.merchant_id && (this.product.merchant_id.includes('-') || !this.product.merchant_id.startsWith('merchant_'))) { + console.log('尝试加载商户信息:', this.product.merchant_id) + try { + const shop = await supabaseService.getShopByMerchantId(this.product.merchant_id) + if (shop) { + console.log('加载到商户信息:', shop.shop_name) + + // 确保字段存在,避免 undefined 导致构造失败 + this.merchant = { + id: shop.id || '', + user_id: shop.merchant_id || '', + shop_name: shop.shop_name || '未命名店铺', + shop_logo: shop.shop_logo || '/static/default-shop.png', + shop_banner: shop.shop_banner || '/static/default-banner.png', + shop_description: shop.description || '', + contact_name: shop.contact_name || '店主', + contact_phone: shop.contact_phone || '', + shop_status: 1, + // 优先使用 avg_rating,没有则使用默认值 + rating: shop.rating_avg !== undefined && shop.rating_avg !== null ? shop.rating_avg : 4.8, + // 使用 order_count 或 product_count 作为销量/活跃度指标,如果没有则默认 0 + total_sales: shop.total_sales !== undefined ? shop.total_sales : (shop.order_count !== undefined ? shop.order_count : 0), + created_at: shop.created_at || new Date().toISOString() + } as MerchantType + realMerchantLoaded = true + } + } catch (e) { + console.error('加载商户信息失败', e) + } + } + + if (!realMerchantLoaded) { + // 根据商家ID生成不同的商家信息 + const merchantIndex = Math.abs(this.product.merchant_id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)) % 5 + const shopNames = ['优质好店', '品牌直营店', '官方旗舰店', '专卖店', '精品小店'] + const shopDescriptions = [ + '专注品质生活', + '品牌官方直营,正品保障', + '厂家直销,价格优惠', + '专注本领域十年老店', + '用心服务每一位顾客' + ] + const contactNames = ['店主小王', '店长小李', '经理小张', '客服小赵', '老板小钱'] + + this.merchant = { + id: this.product.merchant_id, + user_id: 'user_' + (merchantIndex + 1).toString().padStart(3, '0'), + shop_name: shopNames[merchantIndex], + shop_logo: '/static/shop-logo.png', + shop_banner: '/static/shop-banner.png', + shop_description: shopDescriptions[merchantIndex], + contact_name: contactNames[merchantIndex], + contact_phone: '138' + (10000000 + merchantIndex * 1111111).toString().substring(0, 8), + shop_status: 1, + rating: 4.5 + (merchantIndex * 0.1), + total_sales: 10000 + merchantIndex * 5000, + created_at: '2023-06-01' + } } this.loadProductSkus(productId) }, - loadProductSkus(productId: string) { + async loadProductSkus(productId: string) { + // 尝试从数据库加载SKU + try { + const skus = await supabaseService.getProductSkus(productId) + if (skus.length > 0) { + console.log('加载到商品SKU:', skus.length) + this.productSkus = skus.map((sku): ProductSkuType => { + let specs: UTSJSONObject = {} + if (sku.specifications) { + try { + if (typeof sku.specifications === 'string') { + specs = JSON.parse(sku.specifications) as UTSJSONObject + } else { + // 假设已经是对象 + specs = sku.specifications as unknown as UTSJSONObject + } + } catch(e) { + console.error('解析SKU规格失败', e) + } + } + return { + id: sku.id, + product_id: sku.product_id, + sku_code: sku.sku_code, + specifications: specs, + price: sku.price, + stock: sku.stock !== undefined ? sku.stock : 0, + image_url: sku.image_url || '', + status: sku.status !== undefined ? sku.status : 1 + } as ProductSkuType + }) + return + } + } catch (e) { + console.error('Fetch SKUs error', e) + } + // 模拟加载商品SKU数据 const basePrice = this.product.price @@ -620,7 +728,7 @@ export default { return sku.sku_code }, - addToCart() { + async addToCart() { if (!this.selectedSkuId) { uni.showToast({ title: '请选择规格', @@ -629,50 +737,42 @@ export default { return } - // 获取现有购物车数据 - const cartData = uni.getStorageSync('cart') - let cartItems: any[] = [] - - if (cartData) { - try { - cartItems = JSON.parse(cartData as string) as any[] - } catch (e) { - console.error('解析购物车数据失败', e) - } - } - - // 检查商品是否已存在 (同一SKU) - const existingItem = cartItems.find((item: any) => item.id === this.selectedSkuId) - - if (existingItem) { - existingItem.quantity += this.quantity - } else { - // 查找SKU信息 - const sku = this.productSkus.find(s => s.id === this.selectedSkuId) - - // 添加新商品 - cartItems.push({ - id: this.selectedSkuId, // 使用SKU ID作为购物车条目ID - productId: this.product.id, - shopId: this.merchant.id, - shopName: this.merchant.shop_name, - name: this.product.name, - price: sku ? sku.price : this.product.price, - image: (sku && sku.image_url) ? sku.image_url : this.product.images[0], - spec: this.selectedSpec, - quantity: this.quantity, - selected: true - }) - } - - // 保存回存储 - uni.setStorageSync('cart', JSON.stringify(cartItems)) - - // 模拟添加到购物车 - uni.showToast({ - title: '已添加到购物车', - icon: 'success' + // 显示加载中 + uni.showLoading({ + title: '添加中...' }) + + try { + // 调用 Supabase 服务添加到购物车 + // 传递 productId, quantity, skuId + const success = await supabaseService.addToCart( + this.product.id, + this.quantity, + this.selectedSkuId + ) + + uni.hideLoading() + + if (success) { + uni.showToast({ + title: '已添加到购物车', + icon: 'success' + }) + } else { + console.error('添加购物车返回失败') + uni.showToast({ + title: '添加失败,请登录重试', + icon: 'none' + }) + } + } catch (e) { + uni.hideLoading() + console.error('添加购物车异常', e) + uni.showToast({ + title: '添加异常', + icon: 'none' + }) + } }, buyNow() { @@ -787,6 +887,14 @@ export default { }) }, + goToShop() { + if (this.merchant.user_id) { + uni.navigateTo({ + url: `/pages/mall/consumer/shop-detail?merchantId=${this.merchant.user_id}` + }) + } + }, + goToCart() { uni.switchTab({ url: '/pages/mall/consumer/cart' diff --git a/pages/mall/consumer/shop-detail.uvue b/pages/mall/consumer/shop-detail.uvue index c2bf505e..cb4d5aa2 100644 --- a/pages/mall/consumer/shop-detail.uvue +++ b/pages/mall/consumer/shop-detail.uvue @@ -44,6 +44,7 @@ + + \ No newline at end of file diff --git a/uni_modules/ak-req/ak-req.uts b/uni_modules/ak-req/ak-req.uts index 1c8b5a11..a33c28cb 100644 --- a/uni_modules/ak-req/ak-req.uts +++ b/uni_modules/ak-req/ak-req.uts @@ -1,4 +1,5 @@ import { AkReqUploadOptions, AkReqOptions, AkReqResponse, AkReqError } from './interface.uts'; +import { SUPA_URL } from '@/ak/config.uts'; // token 持久化 key const ACCESS_TOKEN_KEY = 'akreq_access_token'; @@ -75,7 +76,7 @@ export class AkReq { headers = Object.assign({}, headers, { 'apikey': apikey }) as UTSJSONObject; } try { const res = await this.request({ - url: 'https://ak3.oulog.com/auth/v1/token?grant_type=refresh_token', + url: SUPA_URL + '/auth/v1/token?grant_type=refresh_token', method: 'POST', data: ({ refresh_token: refreshToken } as UTSJSONObject), headers: headers, diff --git a/utils/supabaseService.uts b/utils/supabaseService.uts index 3ee23527..beecb3bb 100644 --- a/utils/supabaseService.uts +++ b/utils/supabaseService.uts @@ -1,9 +1,8 @@ -import { createClient } from '@/components/supadb/aksupa.uts' -import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts' +import supa from '@/components/supadb/aksupainstance.uts' import type { AkReqResponse } from '@/uni_modules/ak-req/index.uts' -// 创建 Supabase 客户端 -const supa = createClient(SUPA_URL, SUPA_KEY) +// 使用单例 Supabase 客户端 +// const supa = createClient(SUPA_URL, SUPA_KEY) // 类型定义 export interface Category { @@ -18,18 +17,46 @@ export interface Category { export interface Product { id: string category_id: string + merchant_id: string name: string description?: string - specification: string + specification?: string price: number + base_price?: number original_price?: number + market_price?: number image?: string - manufacturer: string + main_image_url?: string + image_urls?: string // JSON string + manufacturer?: string sales?: number + sale_count?: number stock?: number + available_stock?: number badge?: string shop_id?: string shop_name?: string + attributes?: string // JSON string + created_at?: string + expiry_date?: string + approval_number?: string + usage?: string + side_effects?: string +} + +export interface Shop { + id: string + merchant_id: string + shop_name: string + shop_logo?: string + shop_banner?: string + description?: string + contact_name?: string + contact_phone?: string + rating_avg?: number + total_sales?: number + product_count?: number + total_sales_count?: number created_at?: string } @@ -73,10 +100,33 @@ export interface PaginatedResponse { hasmore: boolean } +export interface ProductSku { + id: string + product_id: string + sku_code: string + specifications: string // JSON string + price: number + market_price?: number + cost_price?: number + stock?: number + warning_stock?: number + image_url?: string + weight?: number + status?: number + created_at?: string +} + class SupabaseService { // 获取当前用户ID - private getCurrentUserId(): string | null { + public getCurrentUserId(): string | null { try { + // 优先从 Supabase 会话获取 + const session = supa.getSession() + if (session && session.user) { + return session.user.getString('id') + } + + // 后备:尝试从本地存储获取 (兼容旧逻辑) const userId = uni.getStorageSync('user_id') return userId ? userId as string : null } catch (e) { @@ -89,7 +139,7 @@ class SupabaseService { async getCategories(): Promise { try { const response = await supa - .from('categories') + .from('ml_categories') .select('*') .order('name', { ascending: true }) .execute() @@ -114,10 +164,10 @@ class SupabaseService { ): Promise> { try { const response = await supa - .from('products') + .from('ml_products') .select('*', { count: 'exact' }) .eq('category_id', categoryId) - .order('sales', { ascending: false }) + .order('sale_count', { ascending: false }) .page(page) .limit(limit) .execute() @@ -152,6 +202,28 @@ class SupabaseService { } } + // 根据商品ID获取SKU列表 + async getProductSkus(productId: string): Promise { + try { + const response = await supa + .from('ml_product_skus') + .select('*') + .eq('product_id', productId) + .eq('status', 1) + .execute() + + if (response.error) { + console.error('获取商品SKU失败:', response.error) + return [] + } + + return response.data as ProductSku[] + } catch (error) { + console.error('获取商品SKU异常:', error) + return [] + } + } + // 搜索商品 async searchProducts( keyword: string, @@ -162,18 +234,18 @@ class SupabaseService { ): Promise> { try { let query = supa - .from('products') + .from('ml_products') .select('*', { count: 'exact' }) .or(`name.ilike.%${keyword}%,manufacturer.ilike.%${keyword}%,specification.ilike.%${keyword}%`) // 根据sortBy和ascending设置排序 if (sortBy === 'price') { - query = query.order('price', { ascending }) - } else if (sortBy === 'sales') { - query = query.order('sales', { ascending: false }) // 销量总是降序 + query = query.order('base_price', { ascending }) + } else if (sortBy === 'sales' || sortBy === 'sale_count') { + query = query.order('sale_count', { ascending: false }) // 销量总是降序 } else { // 默认按销量降序 - query = query.order('sales', { ascending: false }) + query = query.order('sale_count', { ascending: false }) } const response = await query @@ -215,7 +287,7 @@ class SupabaseService { async getProductById(productId: string): Promise { try { const response = await supa - .from('products') + .from('ml_products') .select('*') .eq('id', productId) .single() @@ -233,11 +305,80 @@ class SupabaseService { } } + // 根据商户ID获取店铺信息 + async getShopByMerchantId(merchantId: string): Promise { + try { + const response = await supa + .from('ml_shops') + .select('*') + .eq('merchant_id', merchantId) + .single() + .executeAs() + + if (response.error) { + console.error('获取店铺信息失败:', response.error) + return null + } + + const data = response.data + if (Array.isArray(data)) { + if (data.length > 0) return data[0] as Shop + return null + } + return data as Shop + } catch (error) { + console.error('获取店铺信息异常:', error) + return null + } + } + + // 根据商户ID获取商品列表 + async getProductsByMerchantId(merchantId: string, page: number = 1, limit: number = 20): Promise> { + try { + const response = await supa + .from('ml_products') + .select('*', { count: 'exact' }) + .eq('merchant_id', merchantId) + .order('created_at', { ascending: false }) + .page(page) + .limit(limit) + .execute() + + if (response.error) { + console.error('获取商户商品失败:', response.error) + return { + data: [], + total: 0, + page, + limit, + hasmore: false + } + } + + return { + data: response.data as Product[], + total: response.total || 0, + page, + limit, + hasmore: response.hasmore || false + } + } catch (error) { + console.error('获取商户商品异常:', error) + return { + data: [], + total: 0, + page, + limit, + hasmore: false + } + } + } + // 获取热销商品(按销量排序) async getHotProducts(limit: number = 10): Promise { try { const response = await supa - .from('products') + .from('ml_products') .select('*') .order('sales', { ascending: false }) .limit(limit) @@ -259,7 +400,7 @@ class SupabaseService { async getProductsByPrice(limit: number = 10, ascending: boolean = true): Promise { try { const response = await supa - .from('products') + .from('ml_products') .select('*') .order('price', { ascending }) .limit(limit) @@ -281,7 +422,7 @@ class SupabaseService { async getProductsByNewest(limit: number = 10): Promise { try { const response = await supa - .from('products') + .from('ml_products') .select('*') .order('created_at', { ascending: false }) .limit(limit) @@ -304,7 +445,7 @@ class SupabaseService { try { // 直接使用 neq 空字符串查询,忽略 null 值(null 表示没有 badge,不应被推荐) const response = await supa - .from('products') + .from('ml_products') .select('*') .neq('badge', '') .order('sales', { ascending: false }) @@ -328,7 +469,7 @@ class SupabaseService { async getDiscountProducts(limit: number = 10): Promise { try { const response = await supa - .from('products') + .from('ml_products') .select('*') .eq('badge', '特价') .order('sales', { ascending: false }) @@ -369,14 +510,13 @@ class SupabaseService { selected, created_at, updated_at, - products!inner ( + ml_products!inner ( id, name, image, price, specification, - shop_id, - shop_name + merchant_id ) `) .eq('user_id', userId) @@ -387,12 +527,59 @@ class SupabaseService { console.error('获取购物车失败:', response.error) return [] } + + const cartData = response.data as any[] + + // 调试日子:打印购物车数据第一条结构,确认产品字段名 + if (cartData && Array.isArray(cartData) && cartData.length > 0) { + console.log('Cart Item Structure:', JSON.stringify(cartData[0])) + } + + const merchantIds: string[] = [] + + if (cartData && Array.isArray(cartData)) { + for (const item of cartData) { + // PostgREST 返回的关联字段通常与表名一致 + // 尝试获取ml_products,如果为空则尝试products + let product = item['ml_products'] as any + if (!product) { + product = item['products'] as any + } + + if (product && product.merchant_id && !merchantIds.includes(product.merchant_id)) { + merchantIds.push(product.merchant_id as string) + } + } + } + + // 查询店铺信息 + const shopMap = new Map() + if (merchantIds.length > 0) { + const shopRes = await supa + .from('ml_shops') + .select('id, merchant_id, shop_name') + .in('merchant_id', merchantIds) + .execute() + + if (!shopRes.error && shopRes.data != null) { + const shops = shopRes.data as any[] + for (const shop of shops) { + shopMap.set(shop.merchant_id as string, shop) + } + } + } // 处理返回数据,构建CartItem数组 const cartItems: CartItem[] = [] - if (response.data && Array.isArray(response.data)) { - for (const item of response.data) { - const product = item.products as any + if (cartData && Array.isArray(cartData)) { + for (const item of cartData) { + let product = item['ml_products'] as any + if (!product) { + product = item['products'] as any + } + + const merchantId = product?.merchant_id as string + const shopInfo = shopMap.get(merchantId) cartItems.push({ id: item.id as string, @@ -405,8 +592,8 @@ class SupabaseService { product_image: product?.image as string, product_price: product?.price as number, product_specification: product?.specification as string, - shop_id: product?.shop_id as string, - shop_name: product?.shop_name as string, + shop_id: shopInfo ? (shopInfo['id'] as string) : (merchantId || 'unknown_shop'), + shop_name: shopInfo ? (shopInfo['shop_name'] as string) : '未知店铺', created_at: item.created_at as string, updated_at: item.updated_at as string }) @@ -430,27 +617,59 @@ class SupabaseService { } // 检查商品是否已在购物车中 - const existingResponse = await supa + // 注意:必须处理 sku_id 为空的情况,使用 is.null 过滤器 + let query = supa .from('ml_shopping_cart') .select('*') .eq('user_id', userId) .eq('product_id', productId) - .eq('sku_id', skuId || '') - .single() - .execute() + + if (skuId && skuId.length > 0) { + query = query.eq('sku_id', skuId) + } else { + query = query.is('sku_id', null) + } + + const existingResponse = await query.single().execute() + + let existingItem: any | null = null + + if (existingResponse.data != null) { + const rawData = existingResponse.data as any + if (Array.isArray(rawData)) { + if (rawData.length > 0) { + existingItem = rawData[0] + } + } else { + existingItem = rawData + } + } let response - if (existingResponse.data) { + if (existingItem != null) { // 商品已存在,更新数量 - const existingItem = existingResponse.data as any - response = await supa - .from('ml_shopping_cart') - .update({ - quantity: (existingItem.quantity || 0) + quantity, - updated_at: new Date().toISOString() - }) - .eq('id', existingItem.id) - .execute() + console.log('Found existing cart item:', JSON.stringify(existingItem)) + + // 确保 existingItem.id 存在 + const itemId = existingItem['id'] + const itemQty = existingItem['quantity'] + + if (itemId != null) { + const currentQty = typeof itemQty === 'number' ? itemQty : parseInt(String(itemQty || 0)) + const newQty = currentQty + quantity + + response = await supa + .from('ml_shopping_cart') + .update({ + quantity: newQty, + updated_at: new Date().toISOString() + }) + .eq('id', itemId) + .execute() + } else { + console.error('购物车已有商品但缺少ID,无法更新. Data:', JSON.stringify(existingItem)) + return false + } } else { // 商品不存在,添加新记录 response = await supa @@ -671,7 +890,7 @@ class SupabaseService { const response = await supa .from('ml_user_addresses') - .select('*') + .select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail') .eq('user_id', userId) .order('is_default', { ascending: false }) .order('created_at', { ascending: false }) @@ -700,7 +919,7 @@ class SupabaseService { const response = await supa .from('ml_user_addresses') - .select('*') + .select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail') .eq('id', addressId) .eq('user_id', userId) .single() @@ -745,12 +964,12 @@ class SupabaseService { .from('ml_user_addresses') .insert({ user_id: userId, - recipient_name: address.recipient_name, - phone: address.phone, + receiver_name: address.recipient_name, + receiver_phone: address.phone, province: address.province, city: address.city, district: address.district, - detail_address: address.detail_address, + address_detail: address.detail_address, postal_code: address.postal_code || null, is_default: address.is_default || false, created_at: new Date().toISOString(), @@ -792,13 +1011,22 @@ class SupabaseService { if (address.is_default) { await this.clearDefaultAddress(userId) } + + // 构造更新数据,映射字段名到数据库列名 + const updateData = {} + if (address.recipient_name != null) updateData['receiver_name'] = address.recipient_name + if (address.phone != null) updateData['receiver_phone'] = address.phone + if (address.province != null) updateData['province'] = address.province + if (address.city != null) updateData['city'] = address.city + if (address.district != null) updateData['district'] = address.district + if (address.detail_address != null) updateData['address_detail'] = address.detail_address + if (address.postal_code != null) updateData['postal_code'] = address.postal_code + if (address.is_default != null) updateData['is_default'] = address.is_default + updateData['updated_at'] = new Date().toISOString() const response = await supa .from('ml_user_addresses') - .update({ - ...address, - updated_at: new Date().toISOString() - }) + .update(updateData) .eq('id', addressId) .eq('user_id', userId) .execute() @@ -843,6 +1071,298 @@ class SupabaseService { } } + // 清除默认地址(内部使用) + private async clearDefaultAddress(userId: string): Promise { + try { + await supa + .from('ml_user_addresses') + .update({ + is_default: false, + updated_at: new Date().toISOString() + }) + .eq('user_id', userId) + .eq('is_default', true) + .execute() + } catch (error) { + console.error('清除默认地址异常:', error) + } + } + + // 获取用户资料 + async getUserProfile(): Promise { + try { + const userId = this.getCurrentUserId() + if (!userId) return null + + // 联合查询 auth user 和 profile + // 由于 Supabase auth table 不可直接访问,这里查询 ml_user_profiles + const response = await supa + .from('ml_user_profiles') + .select('*') + .eq('user_id', userId) + .single() + .execute() + + if (response.error) { + // 如果不存在 profile,可能只有 auth user,这里暂时返回空或创建默认 + return null + } + return response.data + } catch (e) { + return null + } + } + + // 创建订单 + async createOrder(orderData: { + merchant_id: string, + product_amount: number, + shipping_fee: number, + total_amount: number, + shipping_address: any, + items: any[] + }): Promise { + try { + const userId = this.getCurrentUserId() + if (!userId) return null + + // 生成订单号 + const orderNo = 'ORD' + Date.now() + Math.floor(Math.random() * 1000) + + // 1. 创建主订单 + const orderResponse = await supa + .from('ml_orders') + .insert({ + user_id: userId, + merchant_id: orderData.merchant_id, + order_no: orderNo, + product_amount: orderData.product_amount, + shipping_fee: orderData.shipping_fee, + total_amount: orderData.total_amount, + paid_amount: 0, + shipping_address: JSON.stringify(orderData.shipping_address), + order_status: 1, // 待付款 + payment_status: 1, // 未支付 + shipping_status: 1, // 未发货 + created_at: new Date().toISOString() + }) + .select() + .single() + .execute() + + if (orderResponse.error) { + console.error('创建订单失败:', orderResponse.error) + return null + } + + const orderId = orderResponse.data['id'] as string + + // 2. 创建订单项 + const orderItems = orderData.items.map((item: any) => ({ + order_id: orderId, + product_id: item.product_id, + sku_id: item.sku_id || null, + product_name: item.product_name, + sku_name: item.sku_name || '', + specifications: item.specifications ? JSON.stringify(item.specifications) : '{}', + image_url: item.image_url, + price: item.price, + quantity: item.quantity, + total_amount: item.price * item.quantity, + created_at: new Date().toISOString() + })) + + const itemsResponse = await supa + .from('ml_order_items') + .insert(orderItems) + .execute() + + if (itemsResponse.error) { + console.error('创建订单项失败:', itemsResponse.error) + // 此时应该回滚订单,但这里简化处理 + return null + } + + // 3. 清除购物车中已购买的商品(如果是从购物车购买) + // 这一步通常在前端调用 removeCartItem 或在此处根据参数处理 + + return orderId + } catch (error) { + console.error('创建订单异常:', error) + return null + } + } + + // 获取订单列表 + async getOrders(status: number = 0): Promise { + try { + const userId = this.getCurrentUserId() + if (!userId) return [] + + let query = supa + .from('ml_orders') + .select(` + *, + ml_order_items (*) + `) + .eq('user_id', userId) + .order('created_at', { ascending: false }) + + if (status > 0) { + query = query.eq('order_status', status) + } + + const response = await query.execute() + + if (response.error) { + console.error('获取订单列表失败:', response.error) + return [] + } + + return response.data || [] + } catch (error) { + console.error('获取订单列表异常:', error) + return [] + } + } + + // 获取订单详情 + async getOrderDetail(orderId: string): Promise { + try { + const userId = this.getCurrentUserId() + if (!userId) return null + + const response = await supa + .from('ml_orders') + .select(` + *, + ml_order_items (*), + ml_shops (shop_name, id) + `) + .eq('id', orderId) + .eq('user_id', userId) + .single() + .execute() + + if (response.error) { + return null + } + return response.data + } catch (e) { + return null + } + } + + // 收藏相关 + async checkFavorite(productId: string): Promise { + try { + const userId = this.getCurrentUserId() + if (!userId) return false + + const response = await supa + .from('ml_user_favorites') + .select('id') + .eq('user_id', userId) + .eq('target_id', productId) + .eq('target_type', 1) // 1 for product + .single() + .execute() + + return !!response.data + } catch(e) { + return false + } + } + + async toggleFavorite(productId: string): Promise { + try { + const userId = this.getCurrentUserId() + if (!userId) return false + + // Check if exists + const exists = await this.checkFavorite(productId) + + if (exists) { + // Delete + await supa + .from('ml_user_favorites') + .delete() + .eq('user_id', userId) + .eq('target_id', productId) + .eq('target_type', 1) + .execute() + return false // Now not favorite + } else { + // Add + await supa + .from('ml_user_favorites') + .insert({ + user_id: userId, + target_id: productId, + target_type: 1, + created_at: new Date().toISOString() + }) + .execute() + return true // Now favorite + } + } catch (e) { + return false + } + } + + async getFavorites(): Promise { + try { + const userId = this.getCurrentUserId() + if (!userId) return [] + + // 需要关联查询商品信息 + const response = await supa + .from('ml_user_favorites') + .select(` + id, + target_id, + created_at, + ml_products!target_id ( + id, name, image_urls, main_image_url, price, sales + ) + `) + .eq('user_id', userId) + .eq('target_type', 1) + .order('created_at', { ascending: false }) + .execute() + + if (response.error) return [] + return response.data || [] + } catch (e) { + return [] + } + } + + + async getAddressList(): Promise { + try { + const userId = this.getCurrentUserId() + if (!userId) return [] + + const response = await supa + .from('ml_user_addresses') + .select('*, recipient_name:receiver_name, phone:receiver_phone, detail_address:address_detail') + .eq('user_id', userId) + .order('is_default', { ascending: false }) + .order('created_at', { ascending: false }) + .execute() + + if (response.error) { + console.error('获取地址列表失败:', response.error) + return [] + } + return response.data as UserAddress[] + } catch (e) { + console.error('获取地址列表异常:', e) + return [] + } + } + // 设置默认地址 async setDefaultAddress(addressId: string): Promise { try { @@ -877,31 +1397,6 @@ class SupabaseService { return false } } - - // 清除用户的默认地址(内部方法) - private async clearDefaultAddress(userId: string): Promise { - try { - const response = await supa - .from('ml_user_addresses') - .update({ - is_default: false, - updated_at: new Date().toISOString() - }) - .eq('user_id', userId) - .eq('is_default', true) - .execute() - - if (response.error) { - console.error('清除默认地址失败:', response.error) - return false - } - - return true - } catch (error) { - console.error('清除默认地址异常:', error) - return false - } - } } // 导出单例实例 From 21149dd3febdead4c37d285504ad9f2ddacad123 Mon Sep 17 00:00:00 2001 From: comlibmb <1844410276@qq.com> Date: Mon, 2 Feb 2026 18:09:30 +0800 Subject: [PATCH 15/18] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E7=BC=96=E5=86=99=EF=BC=8C=E5=BC=80=E5=8F=91=E8=A7=84?= =?UTF-8?q?=E8=8C=83=E6=96=87=E6=A1=A3=EF=BC=8C=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/analytics/AnalyticsComboChart.uvue | 5 +- .../analytics/AnalyticsSidebarMenu.uvue | 188 +++++++++- docs/AGENT_PROJECT_SPEC.md | 325 +++++++++++++++++ docs/sql/00_overview.md | 32 ++ docs/sql/01_tables_catalog.md | 236 +++++++++++++ docs/sql/02_relationships_er.md | 147 ++++++++ docs/sql/03_enums_status_dict.md | 213 +++++++++++ docs/sql/04_triggers_and_functions.md | 240 +++++++++++++ docs/sql/05_rls_permissions_matrix.md | 159 +++++++++ docs/sql/06_indexes_and_query_patterns.md | 179 ++++++++++ docs/sql/07_business_workflows.md | 333 ++++++++++++++++++ docs/sql/08_data_consistency_boundaries.md | 116 ++++++ docs/sql/09_migrations_and_versions.md | 126 +++++++ docs/sql/10_quality_checks.md | 91 +++++ docs/sql/11_roles_and_permissions_strategy.md | 184 ++++++++++ docs/sql/README.md | 17 + docs/sql_summary.md | 90 +++++ mall_sql/migrations/ml_analytics_rpcs.sql | 46 ++- pages/mall/analytics/coupon-analysis.uvue | 54 ++- pages/mall/analytics/custom-report.uvue | 30 +- pages/mall/analytics/delivery-analysis.uvue | 52 ++- pages/mall/analytics/index.uvue | 13 +- pages/mall/analytics/market-trends.uvue | 54 ++- pages/mall/analytics/product-insights.uvue | 66 +++- pages/mall/analytics/sales-report.uvue | 5 +- .../test/01_ml_analytics_rpcs_user.sql | 46 ++- pages/mall/analytics/user-analysis.uvue | 50 ++- pages/user/login.uvue | 15 +- pages/user/register.uvue | 17 +- services/analytics/authGuard.uts | 20 ++ services/analytics/couponAnalysisService.uts | 15 +- services/analytics/customReportService.uts | 4 +- .../analytics/deliveryAnalysisService.uts | 14 +- services/analytics/marketTrendsService.uts | 15 +- services/analytics/productInsightsService.uts | 75 +++- utils/authRedirect.uts | 62 ++++ 36 files changed, 3245 insertions(+), 89 deletions(-) create mode 100644 docs/AGENT_PROJECT_SPEC.md create mode 100644 docs/sql/00_overview.md create mode 100644 docs/sql/01_tables_catalog.md create mode 100644 docs/sql/02_relationships_er.md create mode 100644 docs/sql/03_enums_status_dict.md create mode 100644 docs/sql/04_triggers_and_functions.md create mode 100644 docs/sql/05_rls_permissions_matrix.md create mode 100644 docs/sql/06_indexes_and_query_patterns.md create mode 100644 docs/sql/07_business_workflows.md create mode 100644 docs/sql/08_data_consistency_boundaries.md create mode 100644 docs/sql/09_migrations_and_versions.md create mode 100644 docs/sql/10_quality_checks.md create mode 100644 docs/sql/11_roles_and_permissions_strategy.md create mode 100644 docs/sql/README.md create mode 100644 docs/sql_summary.md create mode 100644 services/analytics/authGuard.uts create mode 100644 utils/authRedirect.uts diff --git a/components/analytics/AnalyticsComboChart.uvue b/components/analytics/AnalyticsComboChart.uvue index fd3ef7b2..a328103d 100644 --- a/components/analytics/AnalyticsComboChart.uvue +++ b/components/analytics/AnalyticsComboChart.uvue @@ -154,10 +154,11 @@ export default { data: x, axisTick: { alignWithLabel: true }, axisLine: { lineStyle: { color: 'rgba(0,0,0,0.12)' } }, - axisLabel: { + axisLabel: { color: 'rgba(0,0,0,0.55)', rotate: x.length > 12 ? 45 : 0, - interval: 0 + // 数据量大时不要强制全部展示,否则会全部重叠 + interval: x.length > 60 ? 'auto' : 0 } }, yAxis: [ diff --git a/components/analytics/AnalyticsSidebarMenu.uvue b/components/analytics/AnalyticsSidebarMenu.uvue index a79dab5d..a0d6c18b 100644 --- a/components/analytics/AnalyticsSidebarMenu.uvue +++ b/components/analytics/AnalyticsSidebarMenu.uvue @@ -4,15 +4,32 @@ - - {{ item.icon }} - {{ item.title }} + + {{ item.icon }} + {{ item.title }} + {{ isExpanded(item.path) ? '▾' : '▸' }} + + + + + {{ child.icon }} + {{ child.title }} + + @@ -22,11 +39,15 @@