feat(admin): merge stash changes into comclib-analytics (order/finance/product + rpc sql)

This commit is contained in:
comlibmb
2026-02-10 18:49:21 +08:00
parent bf394eb65d
commit 80e5a1ddeb
23 changed files with 1599 additions and 143 deletions

View File

@@ -0,0 +1,13 @@
-- =====================================================================================
-- User 模块扩展 - 财务字段补全
-- 位置docs/sql/10_schema/user/
-- 版本v1
-- 描述:为 ak_users 增加余额与佣金字段,支持财务业务。
-- =====================================================================================
ALTER TABLE public.ak_users
ADD COLUMN IF NOT EXISTS now_money DECIMAL(12,2) DEFAULT 0,
ADD COLUMN IF NOT EXISTS brokerage_price DECIMAL(12,2) DEFAULT 0;
COMMENT ON COLUMN public.ak_users.now_money IS '用户当前余额';
COMMENT ON COLUMN public.ak_users.brokerage_price IS '用户当前佣金';

View File

@@ -0,0 +1,35 @@
-- =====================================================================================
-- Admin 系统功能 - 获取配置项 RPC
-- 位置docs/sql/30_rpc/admin/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_system_configs, ak_users 表已存在
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_system_config_get(
p_key TEXT
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_value JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取配置值
SELECT config_value INTO v_value
FROM public.ml_system_configs
WHERE config_key = p_key;
RETURN v_value;
END;
$$;

View File

@@ -0,0 +1,37 @@
-- =====================================================================================
-- Admin 系统功能 - 保存配置项 RPC
-- 位置docs/sql/30_rpc/admin/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_system_configs, ak_users 表已存在
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_system_config_save(
p_key TEXT,
p_value JSONB
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 执行保存(存在则更新,不存在则插入)
INSERT INTO public.ml_system_configs (config_key, config_value, updated_at)
VALUES (p_key, p_value, NOW())
ON CONFLICT (config_key)
DO UPDATE SET
config_value = EXCLUDED.config_value,
updated_at = NOW();
RETURN TRUE;
END;
$$;

View File

@@ -0,0 +1,103 @@
-- =====================================================================================
-- Admin 财务功能 - 提现申请列表分页查询 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_extract, ak_users 表已存在
-- 权限:仅 admin 角色可执行(口径 A全局数据访问通过 RPC
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_extract_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_status SMALLINT DEFAULT NULL,
p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_extract e
LEFT JOIN public.ak_users u ON u.id = e.uid
WHERE (p_status IS NULL OR e.status = p_status)
AND (p_start_time IS NULL OR e.created_at >= p_start_time)
AND (p_end_time IS NULL OR e.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.real_name, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.bank_code, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.alipay_code, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.wechat_code, '') ILIKE '%' || p_search || '%'
));
-- 3. 获取明细数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
e.id,
e.uid,
e.real_name,
e.extract_type,
e.bank_code,
e.bank_address,
e.alipay_code,
e.wechat_code,
e.extract_price,
e.service_fee,
e.balance,
e.status,
e.refusal_reason,
e.admin_id,
e.payment_time,
e.created_at,
e.updated_at,
u.username as user_name,
u.email as user_email
FROM public.ml_extract e
LEFT JOIN public.ak_users u ON u.id = e.uid
WHERE (p_status IS NULL OR e.status = p_status)
AND (p_start_time IS NULL OR e.created_at >= p_start_time)
AND (p_end_time IS NULL OR e.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.real_name, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.bank_code, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.alipay_code, '') ILIKE '%' || p_search || '%' OR
COALESCE(e.wechat_code, '') ILIKE '%' || p_search || '%'
))
ORDER BY e.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
-- 4. 返回结果
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_extract_list IS '管理员提现申请列表分页查询';

View File

@@ -0,0 +1,78 @@
-- =====================================================================================
-- Admin 财务功能 - 提现审核 RPC (口径 2)
-- 位置docs/sql/30_rpc/finance/
-- 版本v1
-- 描述:提现审核通过时才扣除佣金并生成流水。
-- 安全策略SECURITY DEFINER, 入口鉴权, 固定 search_path
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_extract_review(
p_extract_id UUID,
p_status SMALLINT, -- 1: 通过, -1: 驳回
p_refusal_reason TEXT DEFAULT NULL
)
RETURNS VOID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_extract RECORD;
v_user RECORD;
BEGIN
-- 1. 鉴权:仅 admin 角色可执行
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 锁定并获取提现记录
SELECT * INTO v_extract FROM public.ml_extract WHERE id = p_extract_id FOR UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'Extract record not found'; END IF;
IF v_extract.status != 0 THEN RAISE EXCEPTION 'Record already processed'; END IF;
-- 3. 业务处理
IF p_status = 1 THEN
-- 审核通过:锁定并校验用户资金
SELECT * INTO v_user FROM public.ak_users WHERE id = v_extract.uid FOR UPDATE;
IF v_user.brokerage_price < v_extract.extract_price THEN
RAISE EXCEPTION 'Insufficient brokerage balance';
END IF;
-- 扣除佣金
UPDATE public.ak_users
SET brokerage_price = brokerage_price - v_extract.extract_price
WHERE id = v_extract.uid;
-- 写入资金流水
INSERT INTO public.ml_user_bill (uid, link_id, pm, title, category, type, number, balance)
VALUES (
v_extract.uid,
p_extract_id::TEXT,
0, -- 支出
'佣金提现',
'brokerage',
'extract',
v_extract.extract_price,
v_user.brokerage_price - v_extract.extract_price
);
-- 更新提现记录
UPDATE public.ml_extract
SET status = 1, admin_id = auth.uid(), payment_time = now()
WHERE id = p_extract_id;
ELSIF p_status = -1 THEN
-- 审核驳回:仅更新状态
UPDATE public.ml_extract
SET status = -1, refusal_reason = p_refusal_reason, admin_id = auth.uid()
WHERE id = p_extract_id;
ELSE
RAISE EXCEPTION 'Invalid status';
END IF;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_extract_review IS '管理员审核提现申请(口径 2通过时扣款';

View File

@@ -0,0 +1,64 @@
-- =====================================================================================
-- Admin 财务功能 - 充值补单/审计 RPC
-- 位置docs/sql/30_rpc/finance/
-- 版本v1
-- 描述:由管理员发起的人工充值补单或离线支付审计确认。
-- 安全策略SECURITY DEFINER, 入口鉴权, 固定 search_path
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_recharge_audit(
p_recharge_id UUID,
p_mark TEXT DEFAULT '管理员人工审计/补单'
)
RETURNS VOID
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_recharge RECORD;
v_user RECORD;
BEGIN
-- 1. 鉴权:仅 admin 角色可执行
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 锁定并获取充值记录
SELECT * INTO v_recharge FROM public.ml_user_recharge WHERE id = p_recharge_id FOR UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'Recharge record not found'; END IF;
IF v_recharge.paid = 1 THEN RAISE EXCEPTION 'Recharge already paid'; END IF;
-- 3. 锁定并更新用户余额
SELECT * INTO v_user FROM public.ak_users WHERE id = v_recharge.uid FOR UPDATE;
UPDATE public.ak_users
SET now_money = now_money + v_recharge.price + v_recharge.give_price
WHERE id = v_recharge.uid;
-- 4. 写入资金流水
INSERT INTO public.ml_user_bill (uid, link_id, pm, title, category, type, number, balance, mark)
VALUES (
v_recharge.uid,
v_recharge.order_no,
1, -- 收入
'用户充值',
'now_money',
'recharge',
v_recharge.price + v_recharge.give_price,
v_user.now_money + v_recharge.price + v_recharge.give_price,
p_mark
);
-- 5. 更新充值记录状态
UPDATE public.ml_user_recharge
SET paid = 1, pay_time = now()
WHERE id = p_recharge_id;
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_recharge_audit IS '管理员人工审计/补单(更新用户余额并生成流水)';

View File

@@ -0,0 +1,92 @@
-- =====================================================================================
-- Admin 财务功能 - 充值记录列表分页查询 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_user_recharge, ak_users 表已存在
-- 权限:仅 admin 角色可执行(口径 A全局数据访问通过 RPC
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_recharge_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_paid SMALLINT DEFAULT NULL,
p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_user_recharge r
LEFT JOIN public.ak_users u ON u.id = r.uid
WHERE (p_paid IS NULL OR r.paid = p_paid)
AND (p_start_time IS NULL OR r.created_at >= p_start_time)
AND (p_end_time IS NULL OR r.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(r.order_no, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%'
));
-- 3. 获取明细数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
r.id,
r.uid,
r.order_no,
r.recharge_type,
r.price,
r.give_price,
r.paid,
r.pay_time,
r.channel_trade_no,
r.status,
r.created_at,
r.updated_at,
u.username as user_name,
u.email as user_email
FROM public.ml_user_recharge r
LEFT JOIN public.ak_users u ON u.id = r.uid
WHERE (p_paid IS NULL OR r.paid = p_paid)
AND (p_start_time IS NULL OR r.created_at >= p_start_time)
AND (p_end_time IS NULL OR r.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(r.order_no, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%'
))
ORDER BY r.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
-- 4. 返回结果
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_recharge_list IS '管理员充值记录列表分页查询';

View File

@@ -0,0 +1,99 @@
-- =====================================================================================
-- Admin 财务功能 - 资金流水列表分页查询 RPC
-- 位置docs/sql/30_rpc/finance/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_user_bill, ak_users 表已存在
-- 权限:仅 admin 角色可执行(口径 A全局数据访问通过 RPC
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_user_bill_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_category VARCHAR DEFAULT NULL,
p_type VARCHAR DEFAULT NULL,
p_pm SMALLINT DEFAULT NULL,
p_start_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_end_time TIMESTAMP WITH TIME ZONE DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role = 'admin'
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_user_bill b
LEFT JOIN public.ak_users u ON u.id = b.uid
WHERE (p_category IS NULL OR b.category = p_category)
AND (p_type IS NULL OR b.type = p_type)
AND (p_pm IS NULL OR b.pm = p_pm)
AND (p_start_time IS NULL OR b.created_at >= p_start_time)
AND (p_end_time IS NULL OR b.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(b.title, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%'
));
-- 3. 获取明细数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
b.id,
b.uid,
b.link_id,
b.pm,
b.title,
b.category,
b.type,
b.number,
b.balance,
b.mark,
b.status,
b.created_at,
b.updated_at,
u.username as user_name,
u.email as user_email
FROM public.ml_user_bill b
LEFT JOIN public.ak_users u ON u.id = b.uid
WHERE (p_category IS NULL OR b.category = p_category)
AND (p_type IS NULL OR b.type = p_type)
AND (p_pm IS NULL OR b.pm = p_pm)
AND (p_start_time IS NULL OR b.created_at >= p_start_time)
AND (p_end_time IS NULL OR b.created_at <= p_end_time)
AND (p_search IS NULL OR (
COALESCE(b.title, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.username, '') ILIKE '%' || p_search || '%' OR
COALESCE(u.email, '') ILIKE '%' || p_search || '%'
))
ORDER BY b.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
-- 4. 返回结果
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;
COMMENT ON FUNCTION public.rpc_admin_user_bill_list IS '管理员资金流水列表分页查询';

View File

@@ -0,0 +1,68 @@
-- =====================================================================================
-- Admin 订单功能 - 收银台订单列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_cashier_order_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_search_order_no TEXT DEFAULT NULL,
p_search_username TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 总数:仅已支付订单
SELECT COUNT(*) INTO v_total
FROM public.ml_orders o
LEFT JOIN public.ak_users u ON o.user_id = u.id
WHERE o.paid_at IS NOT NULL
AND (p_search_order_no IS NULL OR o.order_no ILIKE '%' || p_search_order_no || '%')
AND (p_search_username IS NULL OR u.username ILIKE '%' || p_search_username || '%');
-- 3. 明细
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
o.id,
o.order_no,
o.total_amount,
o.discount_amount,
o.paid_at,
u.username as customer_name,
u.phone as customer_phone
FROM public.ml_orders o
LEFT JOIN public.ak_users u ON o.user_id = u.id
WHERE o.paid_at IS NOT NULL
AND (p_search_order_no IS NULL OR o.order_no ILIKE '%' || p_search_order_no || '%')
AND (p_search_username IS NULL OR u.username ILIKE '%' || p_search_username || '%')
ORDER BY o.paid_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;

View File

@@ -0,0 +1,87 @@
-- =====================================================================================
-- Admin 订单功能 - 售后退款列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_refund_order_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_refund_status INTEGER DEFAULT NULL,
p_search TEXT DEFAULT NULL
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数
SELECT COUNT(*) INTO v_total
FROM public.ml_refund_orders ro
WHERE (p_refund_status IS NULL OR ro.refund_status = p_refund_status)
AND (p_search IS NULL OR (
ro.refund_no ILIKE '%' || p_search || '%' OR
EXISTS (
SELECT 1 FROM public.ml_orders o
WHERE o.id = ro.order_id AND o.order_no ILIKE '%' || p_search || '%'
)
));
-- 3. 获取明细数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
ro.id,
ro.refund_no,
ro.refund_amount,
ro.refund_status,
ro.refund_reason,
ro.applied_at,
o.order_no,
o.order_status,
u.username as customer_name,
u.phone as customer_phone,
(
SELECT jsonb_build_object(
'product_name', oi.product_name,
'image_url', oi.image_url
)
FROM public.ml_order_items oi
WHERE oi.order_id = ro.order_id
LIMIT 1
) as product_summary
FROM public.ml_refund_orders ro
LEFT JOIN public.ml_orders o ON ro.order_id = o.id
LEFT JOIN public.ak_users u ON ro.user_id = u.id
WHERE (p_refund_status IS NULL OR ro.refund_status = p_refund_status)
AND (p_search IS NULL OR (
ro.refund_no ILIKE '%' || p_search || '%' OR
o.order_no ILIKE '%' || p_search || '%'
))
ORDER BY ro.applied_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;

View File

@@ -0,0 +1,80 @@
-- =====================================================================================
-- Admin 订单功能 - 核销记录列表分页查询 RPC
-- 位置docs/sql/30_rpc/order/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_write_off_record_list(
p_page INTEGER DEFAULT 1,
p_page_size INTEGER DEFAULT 15,
p_search TEXT DEFAULT NULL,
p_verified_only BOOLEAN DEFAULT TRUE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_offset INTEGER;
v_total BIGINT;
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
v_offset := (p_page - 1) * p_page_size;
-- 2. 获取总数(核销订单类型 = 3
SELECT COUNT(*) INTO v_total
FROM public.ml_orders o
WHERE o.order_type = 3
AND (p_verified_only = FALSE OR o.verified_at IS NOT NULL)
AND (p_search IS NULL OR o.order_no ILIKE '%' || p_search || '%');
-- 3. 获取明细
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
o.id,
o.order_no,
o.total_amount,
o.payment_status,
o.order_status,
o.created_at,
o.verified_at,
buyer.username as customer_name,
buyer.phone as customer_phone,
verifier.username as verifier_name,
(
SELECT jsonb_build_object(
'product_name', oi.product_name,
'image_url', oi.image_url
)
FROM public.ml_order_items oi
WHERE oi.order_id = o.id
LIMIT 1
) as product_summary
FROM public.ml_orders o
LEFT JOIN public.ak_users buyer ON o.user_id = buyer.id
LEFT JOIN public.ak_users verifier ON o.verifier_id = verifier.id
WHERE o.order_type = 3
AND (p_verified_only = FALSE OR o.verified_at IS NOT NULL)
AND (p_search IS NULL OR o.order_no ILIKE '%' || p_search || '%')
ORDER BY o.verified_at DESC NULLS LAST, o.created_at DESC
LIMIT p_page_size
OFFSET v_offset
) t;
RETURN jsonb_build_object(
'total', v_total,
'items', COALESCE(v_items, '[]'::jsonb)
);
END;
$$;

View File

@@ -0,0 +1,46 @@
-- =====================================================================================
-- Admin 商品模块 - 删除分类 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 方案:方案 1有子项禁止删除
-- 版本v1
-- 依赖ml_categories, ak_users 表已存在
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_category_delete(
p_id UUID
)
RETURNS BOOLEAN
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 检查是否有子分类 (方案 1)
IF EXISTS (
SELECT 1 FROM public.ml_categories WHERE parent_id = p_id
) THEN
RAISE EXCEPTION '请先删除该分类下的子分类';
END IF;
-- 3. 检查是否有商品关联 (可选,通常作为安全保障)
IF EXISTS (
SELECT 1 FROM public.ml_products WHERE category_id = p_id AND status != 4
) THEN
RAISE EXCEPTION '该分类下仍有商品,无法删除';
END IF;
-- 4. 执行删除
DELETE FROM public.ml_categories WHERE id = p_id;
RETURN FOUND;
END;
$$;

View File

@@ -0,0 +1,105 @@
-- =====================================================================================
-- Admin 商品模块 - 商品统计概况 RPC
-- 位置docs/sql/30_rpc/product/
-- 对象类型RPC 函数SECURITY DEFINER
-- 版本v1
-- 依赖ml_products, ml_orders, ml_browse_history, ak_users
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_product_stats(
p_start_time TIMESTAMP WITH TIME ZONE,
p_end_time TIMESTAMP WITH TIME ZONE
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_stats JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 统计核心指标
-- 商品浏览量 (PV), 访客数 (UV), 支付件数, 支付金额, 退款件数, 退款金额
WITH stats AS (
SELECT
(SELECT COALESCE(SUM(browse_duration), 0) FROM public.ml_browse_history WHERE created_at BETWEEN p_start_time AND p_end_time) as total_views,
(SELECT COUNT(DISTINCT user_id) FROM public.ml_browse_history WHERE created_at BETWEEN p_start_time AND p_end_time) as total_visitors,
(SELECT COALESCE(SUM(quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
WHERE o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as pay_count,
(SELECT COALESCE(SUM(paid_amount), 0) FROM public.ml_orders
WHERE created_at BETWEEN p_start_time AND p_end_time AND order_status NOT IN (1, 5)) as pay_amount,
(SELECT COUNT(*) FROM public.ml_orders WHERE created_at BETWEEN p_start_time AND p_end_time AND order_status = 7) as refund_count,
(SELECT COALESCE(SUM(total_amount), 0) FROM public.ml_orders WHERE created_at BETWEEN p_start_time AND p_end_time AND order_status = 7) as refund_amount
)
SELECT jsonb_build_object(
'views', total_views,
'visitors', total_visitors,
'pay_count', pay_count,
'pay_amount', pay_amount,
'refund_count', refund_count,
'refund_amount', refund_amount
) INTO v_stats FROM stats;
RETURN v_stats;
END;
$$;
-- =====================================================================================
-- Admin 商品模块 - 商品排行 RPC
-- =====================================================================================
CREATE OR REPLACE FUNCTION public.rpc_admin_product_ranking(
p_start_time TIMESTAMP WITH TIME ZONE,
p_end_time TIMESTAMP WITH TIME ZONE,
p_sort_by TEXT DEFAULT 'sales', -- views, sales, amount
p_limit INTEGER DEFAULT 10
)
RETURNS JSONB
SECURITY DEFINER
SET search_path = public
LANGUAGE plpgsql
AS $$
DECLARE
v_items JSONB;
BEGIN
-- 1. 权限检查
IF NOT EXISTS (
SELECT 1 FROM public.ak_users
WHERE id = auth.uid() AND role IN ('admin', 'analytics')
) THEN
RAISE EXCEPTION 'Permission denied';
END IF;
-- 2. 获取排行数据
SELECT jsonb_agg(t) INTO v_items
FROM (
SELECT
p.id,
p.name,
p.main_image_url as image,
COALESCE(p.view_count, 0) as views,
(SELECT COUNT(DISTINCT user_id) FROM public.ml_browse_history bh WHERE bh.product_id = p.id AND bh.created_at BETWEEN p_start_time AND p_end_time) as visitors,
(SELECT COALESCE(SUM(quantity), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as sales,
(SELECT COALESCE(SUM(oi.total_amount), 0) FROM public.ml_order_items oi JOIN public.ml_orders o ON oi.order_id = o.id
WHERE oi.product_id = p.id AND o.created_at BETWEEN p_start_time AND p_end_time AND o.order_status NOT IN (1, 5)) as amount
FROM public.ml_products p
WHERE p.status != 4
ORDER BY
CASE WHEN p_sort_by = 'views' THEN 4
WHEN p_sort_by = 'sales' THEN 6
WHEN p_sort_by = 'amount' THEN 7
ELSE 6 END DESC
LIMIT p_limit
) t;
RETURN COALESCE(v_items, '[]'::jsonb);
END;
$$;