From 6716398175f0553735db60488e515215a7c8e730 Mon Sep 17 00:00:00 2001
From: comlibmb <1844410276@qq.com>
Date: Sat, 31 Jan 2026 21:47:42 +0800
Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=86=E6=9E=90ui=E8=A1=A5?=
=?UTF-8?q?=E5=85=85=E5=AE=8C=E5=96=84=EF=BC=8C=E6=8E=A5=E5=85=A5=E6=95=B0?=
=?UTF-8?q?=E6=8D=AE=E5=BA=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
mall_sql/migrations/analytics_upgrade.sql | 274 ++++
mall_sql/migrations/ml_analytics_rpcs.sql | 220 +++
pages/mall/analytics/coupon-analysis.uvue | 679 +++++----
pages/mall/analytics/custom-report.uvue | 384 +++++-
pages/mall/analytics/data-detail.uvue | 1209 ++++++-----------
pages/mall/analytics/delivery-analysis.uvue | 625 +++++----
pages/mall/analytics/index.uvue | 1155 +++++++---------
pages/mall/analytics/insight-detail.uvue | 728 +++-------
pages/mall/analytics/market-trends.uvue | 578 ++++----
pages/mall/analytics/product-insights.uvue | 625 +++++----
pages/mall/analytics/profile.uvue | 138 +-
pages/mall/analytics/report-detail.uvue | 647 ++++-----
pages/mall/analytics/sales-report.uvue | 385 +++---
.../test/01_create_coupon_tables.sql | 223 ---
.../test/01_create_delivery_tables.sql | 283 ----
.../mall/analytics/test/01_create_tables.sql | 303 -----
.../test/01_ml_analytics_rpcs_user.sql | 206 +++
.../test/02_insert_coupon_test_data.sql | 182 ---
.../test/02_insert_delivery_test_data.sql | 72 -
.../02_insert_market_trends_test_data.sql | 160 ---
.../analytics/test/02_insert_test_data.sql | 172 ---
.../test/02_ml_analytics_rpcs_sales.sql | 176 +++
.../test/03_delivery_rls_policies.sql | 222 ---
.../test/03_ml_analytics_rpcs_dashboard.sql | 76 ++
.../test/03_orders_analytics_policies.sql | 70 -
pages/mall/analytics/test/03_test_queries.sql | 225 ---
pages/mall/analytics/test/04_cleanup.sql | 110 --
.../04_ml_analytics_rpcs_market_trends.sql | 175 +++
.../test/05_ml_analytics_rpcs_product.sql | 194 +++
.../analytics/test/05_product_trend_rpc.sql | 68 -
.../analytics/test/06_market_trends_rpcs.sql | 171 ---
.../test/06_ml_analytics_rpcs_coupon.sql | 183 +++
.../analytics/test/07_custom_report_rpcs.sql | 405 ------
.../test/07_ml_analytics_rpcs_delivery.sql | 85 ++
.../08_ml_analytics_rpcs_custom_report.sql | 145 ++
.../test/08_ml_analytics_rpcs_data_detail.sql | 53 +
.../test/09_ml_analytics_rpcs_data_detail.sql | 239 ++++
.../analytics/test/99_ml_analytics_verify.sql | 218 +++
.../test/ANALYTICS_DATA_QUICK_START.md | 66 -
.../analytics/test/ANALYTICS_DB_SCHEMA.sql | 853 ------------
.../analytics/test/ANALYTICS_TEST_SEED.sql | 463 -------
.../analytics/test/COUPON_ANALYSIS_RPCS.sql | 388 ------
.../mall/analytics/test/DATA_DETAIL_RPCS.sql | 342 -----
.../analytics/test/DELIVERY_ANALYSIS_RPCS.sql | 73 -
pages/mall/analytics/test/README.md | 178 ---
.../analytics/test/SQL_EXECUTION_ORDER.md | 15 -
pages/mall/analytics/test/SQL_USAGE_GUIDE.md | 274 ----
.../analytics/test/TEST_DATA_INSERT_GUIDE.md | 209 ---
.../mall/analytics/test/test-connection.uvue | 529 --------
pages/mall/analytics/user-analysis.uvue | 953 ++++++++-----
services/analytics/couponAnalysisService.uts | 38 +-
services/analytics/customReportService.uts | 58 +-
services/analytics/dashboardService.uts | 63 +-
services/analytics/dataDetailService.uts | 98 +-
.../analytics/deliveryAnalysisService.uts | 122 +-
services/analytics/productInsightsService.uts | 25 +-
services/analytics/salesReportService.uts | 27 +-
types/analytics.uts | 3 +
types/analytics/common.uts | 5 +
types/analytics/coupon.uts | 11 +
types/analytics/custom-report.uts | 29 +
types/analytics/dashboard.uts | 7 +
types/analytics/data-detail.uts | 4 +
types/analytics/delivery.uts | 25 +
types/analytics/insight.uts | 19 +
types/analytics/market.uts | 9 +
types/analytics/product.uts | 14 +
types/analytics/profile.uts | 38 +
types/analytics/report-detail.uts | 46 +
types/analytics/sales.uts | 22 +
types/analytics/user.uts | 25 +
71 files changed, 6501 insertions(+), 10593 deletions(-)
create mode 100644 mall_sql/migrations/analytics_upgrade.sql
create mode 100644 mall_sql/migrations/ml_analytics_rpcs.sql
delete mode 100644 pages/mall/analytics/test/01_create_coupon_tables.sql
delete mode 100644 pages/mall/analytics/test/01_create_delivery_tables.sql
delete mode 100644 pages/mall/analytics/test/01_create_tables.sql
create mode 100644 pages/mall/analytics/test/01_ml_analytics_rpcs_user.sql
delete mode 100644 pages/mall/analytics/test/02_insert_coupon_test_data.sql
delete mode 100644 pages/mall/analytics/test/02_insert_delivery_test_data.sql
delete mode 100644 pages/mall/analytics/test/02_insert_market_trends_test_data.sql
delete mode 100644 pages/mall/analytics/test/02_insert_test_data.sql
create mode 100644 pages/mall/analytics/test/02_ml_analytics_rpcs_sales.sql
delete mode 100644 pages/mall/analytics/test/03_delivery_rls_policies.sql
create mode 100644 pages/mall/analytics/test/03_ml_analytics_rpcs_dashboard.sql
delete mode 100644 pages/mall/analytics/test/03_orders_analytics_policies.sql
delete mode 100644 pages/mall/analytics/test/03_test_queries.sql
delete mode 100644 pages/mall/analytics/test/04_cleanup.sql
create mode 100644 pages/mall/analytics/test/04_ml_analytics_rpcs_market_trends.sql
create mode 100644 pages/mall/analytics/test/05_ml_analytics_rpcs_product.sql
delete mode 100644 pages/mall/analytics/test/05_product_trend_rpc.sql
delete mode 100644 pages/mall/analytics/test/06_market_trends_rpcs.sql
create mode 100644 pages/mall/analytics/test/06_ml_analytics_rpcs_coupon.sql
delete mode 100644 pages/mall/analytics/test/07_custom_report_rpcs.sql
create mode 100644 pages/mall/analytics/test/07_ml_analytics_rpcs_delivery.sql
create mode 100644 pages/mall/analytics/test/08_ml_analytics_rpcs_custom_report.sql
create mode 100644 pages/mall/analytics/test/08_ml_analytics_rpcs_data_detail.sql
create mode 100644 pages/mall/analytics/test/09_ml_analytics_rpcs_data_detail.sql
create mode 100644 pages/mall/analytics/test/99_ml_analytics_verify.sql
delete mode 100644 pages/mall/analytics/test/ANALYTICS_DATA_QUICK_START.md
delete mode 100644 pages/mall/analytics/test/ANALYTICS_DB_SCHEMA.sql
delete mode 100644 pages/mall/analytics/test/ANALYTICS_TEST_SEED.sql
delete mode 100644 pages/mall/analytics/test/COUPON_ANALYSIS_RPCS.sql
delete mode 100644 pages/mall/analytics/test/DATA_DETAIL_RPCS.sql
delete mode 100644 pages/mall/analytics/test/DELIVERY_ANALYSIS_RPCS.sql
delete mode 100644 pages/mall/analytics/test/README.md
delete mode 100644 pages/mall/analytics/test/SQL_EXECUTION_ORDER.md
delete mode 100644 pages/mall/analytics/test/SQL_USAGE_GUIDE.md
delete mode 100644 pages/mall/analytics/test/TEST_DATA_INSERT_GUIDE.md
delete mode 100644 pages/mall/analytics/test/test-connection.uvue
create mode 100644 types/analytics.uts
create mode 100644 types/analytics/common.uts
create mode 100644 types/analytics/coupon.uts
create mode 100644 types/analytics/custom-report.uts
create mode 100644 types/analytics/dashboard.uts
create mode 100644 types/analytics/data-detail.uts
create mode 100644 types/analytics/delivery.uts
create mode 100644 types/analytics/insight.uts
create mode 100644 types/analytics/market.uts
create mode 100644 types/analytics/product.uts
create mode 100644 types/analytics/profile.uts
create mode 100644 types/analytics/report-detail.uts
create mode 100644 types/analytics/sales.uts
create mode 100644 types/analytics/user.uts
diff --git a/mall_sql/migrations/analytics_upgrade.sql b/mall_sql/migrations/analytics_upgrade.sql
new file mode 100644
index 00000000..3093daab
--- /dev/null
+++ b/mall_sql/migrations/analytics_upgrade.sql
@@ -0,0 +1,274 @@
+-- =====================================================================================
+-- 商城分析模块增量升级脚本
+-- 包含: 埋点事件表、分析用 RPC 函数
+-- =====================================================================================
+
+-- =====================================================================================
+-- 1. 创建用户行为事件表 (埋点)
+-- =====================================================================================
+
+CREATE TABLE IF NOT EXISTS public.analytics_events (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ event_type VARCHAR(50) NOT NULL, -- e.g., page_view, product_view, add_to_cart, checkout_start, payment_success
+ user_id UUID REFERENCES public.ak_users(id) ON DELETE SET NULL,
+ session_id VARCHAR(100),
+ device_id VARCHAR(100),
+ page_url TEXT,
+ referrer TEXT,
+ utm_source VARCHAR(100),
+ utm_medium VARCHAR(100),
+ utm_campaign VARCHAR(100),
+ ip_address INET,
+ user_agent TEXT,
+ screen_resolution VARCHAR(20),
+ device_type VARCHAR(50),
+ os VARCHAR(50),
+ browser VARCHAR(50),
+ country VARCHAR(50),
+ region VARCHAR(50),
+ city VARCHAR(50),
+ event_props JSONB DEFAULT '{}',
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+COMMENT ON TABLE public.analytics_events IS '用户行为事件(埋点)表';
+COMMENT ON COLUMN public.analytics_events.event_type IS '事件类型,如 page_view, product_view, add_to_cart 等';
+
+-- 创建索引
+CREATE INDEX IF NOT EXISTS idx_analytics_events_user_id ON public.analytics_events(user_id);
+CREATE INDEX IF NOT EXISTS idx_analytics_events_event_type ON public.analytics_events(event_type);
+CREATE INDEX IF NOT EXISTS idx_analytics_events_created_at ON public.analytics_events(created_at);
+CREATE INDEX IF NOT EXISTS idx_analytics_events_session_id ON public.analytics_events(session_id);
+
+
+-- =====================================================================================
+-- 2. 创建分析所需的 RPC 函数
+-- =====================================================================================
+
+-- -------------------------------------------------------------------------------------
+-- 函数: rpc_analytics_user_kpis
+-- 描述: 获取用户分析核心KPI指标
+-- -------------------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.rpc_analytics_user_kpis(
+ p_start_date DATE,
+ p_end_date DATE
+)
+RETURNS TABLE (
+ total_users BIGINT,
+ user_growth FLOAT,
+ new_users BIGINT,
+ new_user_growth FLOAT,
+ active_users BIGINT,
+ active_growth FLOAT,
+ ordering_users BIGINT,
+ ordering_growth FLOAT,
+ paid_users BIGINT,
+ paid_growth FLOAT,
+ new_user_conversion_rate FLOAT,
+ repurchase_rate FLOAT,
+ repurchase_growth FLOAT
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ prev_start_date DATE;
+ prev_end_date DATE;
+ period_days INT;
+BEGIN
+ -- 计算上一周期日期范围
+ period_days := p_end_date - p_start_date + 1;
+ prev_start_date := p_start_date - period_days;
+ prev_end_date := p_start_date - 1;
+
+ RETURN QUERY
+ WITH
+ -- 当前周期数据
+ current_period AS (
+ SELECT
+ (SELECT COUNT(*) FROM public.ak_users WHERE created_at <= p_end_date::timestamp) AS total_users,
+ COUNT(DISTINCT CASE WHEN u.created_at::DATE BETWEEN p_start_date AND p_end_date THEN u.id END)::BIGINT AS new_users,
+ COUNT(DISTINCT e.user_id)::BIGINT AS active_users,
+ COUNT(DISTINCT o.user_id)::BIGINT AS ordering_users,
+ COUNT(DISTINCT CASE WHEN o.payment_status = 2 THEN o.user_id END)::BIGINT AS paid_users,
+ (
+ SELECT COUNT(DISTINCT user_id)
+ FROM public.ml_orders
+ WHERE created_at::DATE BETWEEN p_start_date AND p_end_date
+ AND user_id IN (SELECT id FROM public.ak_users WHERE created_at::DATE BETWEEN p_start_date AND p_end_date)
+ ) * 100.0 / NULLIF(COUNT(DISTINCT CASE WHEN u.created_at::DATE BETWEEN p_start_date AND p_end_date THEN u.id END), 0) AS new_user_conversion_rate,
+ COUNT(DISTINCT CASE WHEN user_orders.order_count > 1 THEN user_orders.user_id END) * 100.0 / NULLIF(COUNT(DISTINCT user_orders.user_id), 0) AS repurchase_rate
+ FROM public.ak_users u
+ LEFT JOIN public.analytics_events e ON e.user_id = u.id AND e.created_at::DATE BETWEEN p_start_date AND p_end_date
+ LEFT JOIN public.ml_orders o ON o.user_id = u.id AND o.created_at::DATE BETWEEN p_start_date AND p_end_date
+ LEFT JOIN (
+ SELECT user_id, COUNT(*) as order_count
+ FROM public.ml_orders
+ WHERE created_at::DATE <= p_end_date AND payment_status = 2
+ GROUP BY user_id
+ ) user_orders ON user_orders.user_id = o.user_id AND o.payment_status = 2
+ ),
+ -- 上一周期数据
+ previous_period AS (
+ SELECT
+ (SELECT COUNT(*) FROM public.ak_users WHERE created_at <= prev_end_date::timestamp) AS total_users,
+ COUNT(DISTINCT CASE WHEN u.created_at::DATE BETWEEN prev_start_date AND prev_end_date THEN u.id END)::BIGINT AS new_users,
+ COUNT(DISTINCT e.user_id)::BIGINT AS active_users,
+ COUNT(DISTINCT o.user_id)::BIGINT AS ordering_users,
+ COUNT(DISTINCT CASE WHEN o.payment_status = 2 THEN o.user_id END)::BIGINT AS paid_users,
+ COUNT(DISTINCT CASE WHEN user_orders.order_count > 1 THEN user_orders.user_id END) * 100.0 / NULLIF(COUNT(DISTINCT user_orders.user_id), 0) AS repurchase_rate
+ FROM public.ak_users u
+ LEFT JOIN public.analytics_events e ON e.user_id = u.id AND e.created_at::DATE BETWEEN prev_start_date AND prev_end_date
+ LEFT JOIN public.ml_orders o ON o.user_id = u.id AND o.created_at::DATE BETWEEN prev_start_date AND prev_end_date
+ LEFT JOIN (
+ SELECT user_id, COUNT(*) as order_count
+ FROM public.ml_orders
+ WHERE created_at::DATE <= prev_end_date AND payment_status = 2
+ GROUP BY user_id
+ ) user_orders ON user_orders.user_id = o.user_id AND o.payment_status = 2
+ )
+ SELECT
+ cp.total_users,
+ ROUND(((cp.total_users - pp.total_users) * 100.0 / NULLIF(pp.total_users, 1))::numeric, 2)::FLOAT AS user_growth,
+ cp.new_users,
+ ROUND(((cp.new_users - pp.new_users) * 100.0 / NULLIF(pp.new_users, 1))::numeric, 2)::FLOAT AS new_user_growth,
+ cp.active_users,
+ ROUND(((cp.active_users - pp.active_users) * 100.0 / NULLIF(pp.active_users, 1))::numeric, 2)::FLOAT AS active_growth,
+ cp.ordering_users,
+ ROUND(((cp.ordering_users - pp.ordering_users) * 100.0 / NULLIF(pp.ordering_users, 1))::numeric, 2)::FLOAT AS ordering_growth,
+ cp.paid_users,
+ ROUND(((cp.paid_users - pp.paid_users) * 100.0 / NULLIF(pp.paid_users, 1))::numeric, 2)::FLOAT AS paid_growth,
+ ROUND(cp.new_user_conversion_rate::numeric, 2)::FLOAT,
+ ROUND(cp.repurchase_rate::numeric, 2)::FLOAT AS repurchase_rate,
+ ROUND((cp.repurchase_rate - pp.repurchase_rate)::numeric, 2)::FLOAT AS repurchase_growth
+ FROM current_period cp, previous_period pp;
+END;
+$$;
+
+-- -------------------------------------------------------------------------------------
+-- 函数: rpc_analytics_user_growth_trend
+-- 描述: 获取用户增长趋势(日维度)
+-- -------------------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.rpc_analytics_user_growth_trend(
+ p_start_date DATE,
+ p_end_date DATE
+)
+RETURNS TABLE (
+ date DATE,
+ new_users BIGINT,
+ active_users BIGINT
+)
+LANGUAGE sql
+AS $$
+ WITH date_series AS (
+ SELECT generate_series(p_start_date, p_end_date, '1 day'::interval)::DATE AS date
+ )
+ SELECT
+ ds.date,
+ COUNT(DISTINCT CASE WHEN u.created_at::DATE = ds.date THEN u.id END)::BIGINT AS new_users,
+ COUNT(DISTINCT e.user_id)::BIGINT AS active_users
+ FROM date_series ds
+ LEFT JOIN public.ak_users u ON u.created_at::DATE = ds.date
+ LEFT JOIN public.analytics_events e ON e.created_at::DATE = ds.date
+ GROUP BY ds.date
+ ORDER BY ds.date;
+$$;
+
+-- -------------------------------------------------------------------------------------
+-- 函数: rpc_analytics_user_segments
+-- 描述: 获取用户分群(基于新老客、复购、回流的简单模型)
+-- -------------------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.rpc_analytics_user_segments(
+ p_start_date DATE,
+ p_end_date DATE
+)
+RETURNS TABLE (
+ name TEXT,
+ value BIGINT
+)
+LANGUAGE sql
+AS $$
+ WITH user_orders AS (
+ SELECT
+ user_id,
+ MIN(created_at) as first_order_time,
+ COUNT(id) as total_orders
+ FROM public.ml_orders
+ WHERE payment_status = 2
+ GROUP BY user_id
+ )
+ SELECT
+ segment as name,
+ COUNT(user_id) as value
+ FROM (
+ SELECT
+ u.id as user_id,
+ CASE
+ WHEN uo.first_order_time::DATE BETWEEN p_start_date AND p_end_date THEN '新客'
+ WHEN uo.total_orders > 1 AND EXISTS (SELECT 1 FROM public.ml_orders WHERE user_id = u.id AND created_at::DATE BETWEEN p_start_date AND p_end_date) THEN '复购客户'
+ WHEN uo.total_orders >= 1 AND EXISTS (SELECT 1 FROM public.ml_orders WHERE user_id = u.id AND created_at::DATE BETWEEN p_start_date AND p_end_date) THEN '回流客户'
+ ELSE '老客'
+ END as segment
+ FROM public.ak_users u
+ JOIN user_orders uo ON u.id = uo.user_id
+ ) segments
+ GROUP BY segment;
+$$;
+
+-- -------------------------------------------------------------------------------------
+-- 函数: rpc_analytics_traffic_sources
+-- 描述: 获取流量来源分布
+-- -------------------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.rpc_analytics_traffic_sources(
+ p_start_date DATE,
+ p_end_date DATE
+)
+RETURNS TABLE (
+ name TEXT,
+ value BIGINT
+)
+LANGUAGE sql
+AS $$
+ SELECT
+ COALESCE(
+ NULLIF(utm_source, ''),
+ CASE
+ WHEN referrer IS NULL OR referrer = '' THEN '直接访问'
+ WHEN referrer ILIKE '%baidu.com%' THEN '百度搜索'
+ WHEN referrer ILIKE '%google.com%' THEN '谷歌搜索'
+ WHEN referrer ILIKE '%bing.com%' THEN '必应搜索'
+ WHEN referrer ILIKE '%sogou.com%' THEN '搜狗搜索'
+ WHEN referrer ILIKE '%toutiao.com%' OR referrer ILIKE '%douyin.com%' THEN '字节系'
+ WHEN referrer ILIKE '%weixin.qq.com%' THEN '微信'
+ ELSE '其他推荐'
+ END
+ ) AS name,
+ COUNT(DISTINCT session_id)::BIGINT AS value
+ FROM public.analytics_events
+ WHERE
+ created_at::DATE BETWEEN p_start_date AND p_end_date
+ AND event_type = 'page_view'
+ GROUP BY name
+ ORDER BY value DESC;
+$$;
+
+-- =====================================================================================
+-- 3. 完成提示
+-- =====================================================================================
+
+DO $$
+BEGIN
+ RAISE NOTICE '=======================================================';
+ RAISE NOTICE '商城分析模块增量升级完成!';
+ RAISE NOTICE '=======================================================';
+ RAISE NOTICE '✓ 埋点事件表(analytics_events)创建完成';
+ RAISE NOTICE '✓ RPC 函数创建完成:';
+ RAISE NOTICE ' - rpc_analytics_user_kpis';
+ RAISE NOTICE ' - rpc_analytics_user_growth_trend';
+ RAISE NOTICE ' - rpc_analytics_user_segments';
+ RAISE NOTICE ' - rpc_analytics_traffic_sources';
+ RAISE NOTICE '=======================================================';
+ RAISE NOTICE '下一步:';
+ RAISE NOTICE '1. 在应用中集成前端埋点,将数据写入 analytics_events 表';
+ RAISE NOTICE '2. 在前端分析页面调用以上 RPC 函数获取数据';
+ RAISE NOTICE '=======================================================';
+END $$;
diff --git a/mall_sql/migrations/ml_analytics_rpcs.sql b/mall_sql/migrations/ml_analytics_rpcs.sql
new file mode 100644
index 00000000..e2563c27
--- /dev/null
+++ b/mall_sql/migrations/ml_analytics_rpcs.sql
@@ -0,0 +1,220 @@
+-- =====================================================================================
+-- 商城分析模块 RPC 函数增量脚本
+-- 依赖: ml_* 系列表, ak_users 表
+-- 目标: 为数据分析模块提供核心数据接口,优先使用现有表,不新建表。
+-- =====================================================================================
+
+-- =====================================================================================
+-- 1. 函数: rpc_analytics_user_kpis
+-- 描述: 获取用户分析核心KPI指标,并与上一周期对比。
+-- =====================================================================================
+CREATE OR REPLACE FUNCTION public.rpc_analytics_user_kpis(
+ p_start_date DATE,
+ p_end_date DATE
+)
+RETURNS TABLE (
+ total_users BIGINT,
+ user_growth FLOAT,
+ new_users BIGINT,
+ new_user_growth FLOAT,
+ active_users BIGINT,
+ active_growth FLOAT,
+ ordering_users BIGINT,
+ ordering_growth FLOAT,
+ paid_users BIGINT,
+ paid_growth FLOAT,
+ new_user_conversion_rate FLOAT,
+ repurchase_rate FLOAT,
+ repurchase_growth FLOAT
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ prev_start_date DATE;
+ prev_end_date DATE;
+ period_days INT;
+BEGIN
+ -- 计算上一周期日期范围
+ period_days := p_end_date - p_start_date + 1;
+ prev_start_date := p_start_date - period_days;
+ prev_end_date := p_start_date - 1;
+
+ RETURN QUERY
+ WITH
+ -- 当前周期数据
+ current_period AS (
+ SELECT
+ (SELECT COUNT(*) FROM public.ak_users WHERE created_at <= p_end_date::timestamp) AS total_users,
+ COUNT(DISTINCT CASE WHEN u.created_at::DATE BETWEEN p_start_date AND p_end_date THEN u.id END)::BIGINT AS new_users,
+ (SELECT COUNT(DISTINCT bh.user_id) FROM public.ml_browse_history bh WHERE bh.created_at::DATE BETWEEN p_start_date AND p_end_date) AS active_users,
+ (SELECT COUNT(DISTINCT o.user_id) FROM public.ml_orders o WHERE o.created_at::DATE BETWEEN p_start_date AND p_end_date) AS ordering_users,
+ (SELECT COUNT(DISTINCT o.user_id) FROM public.ml_orders o WHERE o.created_at::DATE BETWEEN p_start_date AND p_end_date AND o.payment_status = 2) AS paid_users,
+ (
+ SELECT COUNT(DISTINCT o.user_id) * 100.0 / NULLIF(COUNT(DISTINCT u_new.id), 0)
+ FROM public.ak_users u_new
+ LEFT JOIN public.ml_orders o ON u_new.id = o.user_id AND o.payment_status = 2 AND o.created_at::DATE BETWEEN p_start_date AND p_end_date
+ WHERE u_new.created_at::DATE BETWEEN p_start_date AND p_end_date
+ ) AS new_user_conversion_rate,
+ (
+ SELECT COUNT(DISTINCT CASE WHEN user_orders.order_count > 1 THEN user_orders.user_id END) * 100.0 / NULLIF(COUNT(DISTINCT user_orders.user_id), 0)
+ FROM (
+ SELECT user_id, COUNT(id) as order_count
+ FROM public.ml_orders
+ WHERE created_at::DATE <= p_end_date AND payment_status = 2
+ GROUP BY user_id
+ ) user_orders
+ WHERE user_orders.user_id IN (SELECT user_id FROM public.ml_orders WHERE created_at::DATE BETWEEN p_start_date AND p_end_date AND payment_status = 2)
+ ) AS repurchase_rate
+ FROM public.ak_users u
+ ),
+ -- 上一周期数据
+ previous_period AS (
+ SELECT
+ (SELECT COUNT(*) FROM public.ak_users WHERE created_at <= prev_end_date::timestamp) AS total_users,
+ COUNT(DISTINCT CASE WHEN u.created_at::DATE BETWEEN prev_start_date AND prev_end_date THEN u.id END)::BIGINT AS new_users,
+ (SELECT COUNT(DISTINCT bh.user_id) FROM public.ml_browse_history bh WHERE bh.created_at::DATE BETWEEN prev_start_date AND prev_end_date) AS active_users,
+ (SELECT COUNT(DISTINCT o.user_id) FROM public.ml_orders o WHERE o.created_at::DATE BETWEEN prev_start_date AND prev_end_date) AS ordering_users,
+ (SELECT COUNT(DISTINCT o.user_id) FROM public.ml_orders o WHERE o.created_at::DATE BETWEEN prev_start_date AND prev_end_date AND o.payment_status = 2) AS paid_users,
+ (
+ SELECT COUNT(DISTINCT CASE WHEN user_orders.order_count > 1 THEN user_orders.user_id END) * 100.0 / NULLIF(COUNT(DISTINCT user_orders.user_id), 0)
+ FROM (
+ SELECT user_id, COUNT(id) as order_count
+ FROM public.ml_orders
+ WHERE created_at::DATE <= prev_end_date AND payment_status = 2
+ GROUP BY user_id
+ ) user_orders
+ WHERE user_orders.user_id IN (SELECT user_id FROM public.ml_orders WHERE created_at::DATE BETWEEN prev_start_date AND prev_end_date AND payment_status = 2)
+ ) AS repurchase_rate
+ FROM public.ak_users u
+ )
+ SELECT
+ cp.total_users,
+ ROUND(((cp.total_users - pp.total_users) * 100.0 / NULLIF(pp.total_users, 0))::numeric, 2)::FLOAT AS user_growth,
+ cp.new_users,
+ ROUND(((cp.new_users - pp.new_users) * 100.0 / NULLIF(pp.new_users, 0))::numeric, 2)::FLOAT AS new_user_growth,
+ cp.active_users,
+ ROUND(((cp.active_users - pp.active_users) * 100.0 / NULLIF(pp.active_users, 0))::numeric, 2)::FLOAT AS active_growth,
+ cp.ordering_users,
+ ROUND(((cp.ordering_users - pp.ordering_users) * 100.0 / NULLIF(pp.ordering_users, 0))::numeric, 2)::FLOAT AS ordering_growth,
+ cp.paid_users,
+ ROUND(((cp.paid_users - pp.paid_users) * 100.0 / NULLIF(pp.paid_users, 0))::numeric, 2)::FLOAT AS paid_growth,
+ ROUND(cp.new_user_conversion_rate::numeric, 2)::FLOAT,
+ ROUND(cp.repurchase_rate::numeric, 2)::FLOAT AS repurchase_rate,
+ ROUND((COALESCE(cp.repurchase_rate, 0) - COALESCE(pp.repurchase_rate, 0))::numeric, 2)::FLOAT AS repurchase_growth
+ FROM current_period cp, previous_period pp;
+END;
+$$;
+
+-- -------------------------------------------------------------------------------------
+-- 2. 函数: rpc_analytics_user_growth_trend
+-- 描述: 获取用户增长与活跃趋势(日维度)
+-- -------------------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.rpc_analytics_user_growth_trend(
+ p_start_date DATE,
+ p_end_date DATE
+)
+RETURNS TABLE (
+ date DATE,
+ new_users BIGINT,
+ active_users BIGINT
+)
+LANGUAGE sql
+AS $$
+ WITH date_series AS (
+ SELECT generate_series(p_start_date, p_end_date, '1 day'::interval)::DATE AS date
+ )
+ SELECT
+ ds.date,
+ (SELECT COUNT(u.id) FROM public.ak_users u WHERE u.created_at::DATE = ds.date)::BIGINT AS new_users,
+ (SELECT COUNT(DISTINCT bh.user_id) FROM public.ml_browse_history bh WHERE bh.created_at::DATE = ds.date)::BIGINT AS active_users
+ FROM date_series ds
+ ORDER BY ds.date;
+$$;
+
+-- -------------------------------------------------------------------------------------
+-- 3. 函数: rpc_analytics_user_segments
+-- 描述: 获取用户分群(基于新老客、复购的简单模型)
+-- -------------------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.rpc_analytics_user_segments(
+ p_start_date DATE,
+ p_end_date DATE
+)
+RETURNS TABLE (
+ name TEXT,
+ value BIGINT
+)
+LANGUAGE sql
+AS $$
+ WITH user_orders_stats AS (
+ SELECT
+ user_id,
+ MIN(created_at) as first_order_time,
+ COUNT(id) as total_orders
+ FROM public.ml_orders
+ WHERE payment_status = 2
+ GROUP BY user_id
+ ),
+ active_users_in_period AS (
+ SELECT DISTINCT user_id FROM public.ml_orders WHERE created_at::DATE BETWEEN p_start_date AND p_end_date
+ )
+ SELECT
+ segment as name,
+ COUNT(user_id) as value
+ FROM (
+ SELECT
+ auip.user_id,
+ CASE
+ WHEN uos.first_order_time::DATE BETWEEN p_start_date AND p_end_date THEN '新客'
+ WHEN uos.total_orders > 1 THEN '复购客户'
+ ELSE '老客'
+ END as segment
+ FROM active_users_in_period auip
+ JOIN user_orders_stats uos ON auip.user_id = uos.user_id
+ ) segments
+ GROUP BY segment;
+$$;
+
+-- -------------------------------------------------------------------------------------
+-- 4. 函数: rpc_analytics_traffic_sources
+-- 描述: 获取流量来源分布 (基于注册来源)
+-- -------------------------------------------------------------------------------------
+CREATE OR REPLACE FUNCTION public.rpc_analytics_traffic_sources(
+ p_start_date DATE,
+ p_end_date DATE
+)
+RETURNS TABLE (
+ name TEXT,
+ value BIGINT
+)
+LANGUAGE sql
+AS $$
+ SELECT
+ COALESCE(registration_source, '未知') AS name,
+ COUNT(id)::BIGINT AS value
+ FROM public.ak_users
+ WHERE created_at::DATE BETWEEN p_start_date AND p_end_date
+ GROUP BY name
+ ORDER BY value DESC;
+$$;
+
+-- =====================================================================================
+-- 5. 完成提示
+-- =====================================================================================
+
+DO $$
+BEGIN
+ RAISE NOTICE '=======================================================';
+ RAISE NOTICE '商城分析模块 RPC 函数创建/更新完成!';
+ RAISE NOTICE '=======================================================';
+ RAISE NOTICE '✓ RPC 函数创建/更新完成:';
+ RAISE NOTICE ' - rpc_analytics_user_kpis';
+ RAISE NOTICE ' - rpc_analytics_user_growth_trend';
+ RAISE NOTICE ' - rpc_analytics_user_segments';
+ RAISE NOTICE ' - rpc_analytics_traffic_sources';
+ RAISE NOTICE '=======================================================';
+ RAISE NOTICE '下一步:';
+ RAISE NOTICE '1. 在数据库中执行此脚本。';
+ RAISE NOTICE '2. 确认前端 user-analysis.uvue 页面已调用这些 RPC。';
+ RAISE NOTICE '3. 如果缺少活跃数据,请确保 ml_browse_history 表有数据写入。';
+ RAISE NOTICE '=======================================================';
+END $$;
diff --git a/pages/mall/analytics/coupon-analysis.uvue b/pages/mall/analytics/coupon-analysis.uvue
index 153c78da..8eddf244 100644
--- a/pages/mall/analytics/coupon-analysis.uvue
+++ b/pages/mall/analytics/coupon-analysis.uvue
@@ -108,370 +108,353 @@
-
diff --git a/pages/mall/analytics/delivery-analysis.uvue b/pages/mall/analytics/delivery-analysis.uvue
index a267dead..36625af4 100644
--- a/pages/mall/analytics/delivery-analysis.uvue
+++ b/pages/mall/analytics/delivery-analysis.uvue
@@ -106,7 +106,6 @@
-
@@ -115,338 +114,326 @@
-
diff --git a/pages/mall/analytics/market-trends.uvue b/pages/mall/analytics/market-trends.uvue
index 03e0ee5a..cca780fb 100644
--- a/pages/mall/analytics/market-trends.uvue
+++ b/pages/mall/analytics/market-trends.uvue
@@ -93,298 +93,305 @@
-
diff --git a/pages/mall/analytics/product-insights.uvue b/pages/mall/analytics/product-insights.uvue
index c98eb294..1c362575 100644
--- a/pages/mall/analytics/product-insights.uvue
+++ b/pages/mall/analytics/product-insights.uvue
@@ -50,7 +50,7 @@
热销商品
{{ formatInt(productData.hot_products) }}
- 销量 > 100
+ 销量 > 100
库存周转率
@@ -70,7 +70,7 @@
商品销售分析
@@ -167,274 +167,307 @@
-
diff --git a/pages/mall/analytics/sales-report.uvue b/pages/mall/analytics/sales-report.uvue
index a5780af2..d7a974ca 100644
--- a/pages/mall/analytics/sales-report.uvue
+++ b/pages/mall/analytics/sales-report.uvue
@@ -148,242 +148,189 @@
-
diff --git a/pages/mall/analytics/user-analysis.uvue b/pages/mall/analytics/user-analysis.uvue
index 569da41f..3d5c3794 100644
--- a/pages/mall/analytics/user-analysis.uvue
+++ b/pages/mall/analytics/user-analysis.uvue
@@ -27,36 +27,55 @@
-
-
-
- {{ 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 };