feat(admin): merge stash changes into comclib-analytics (order/finance/product + rpc sql)
This commit is contained in:
13
docs/sql/10_schema/user/ak_users_finance_fields_v1.sql
Normal file
13
docs/sql/10_schema/user/ak_users_finance_fields_v1.sql
Normal 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 '用户当前佣金';
|
||||||
35
docs/sql/30_rpc/admin/rpc_admin_system_config_get_v1.sql
Normal file
35
docs/sql/30_rpc/admin/rpc_admin_system_config_get_v1.sql
Normal 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;
|
||||||
|
$$;
|
||||||
37
docs/sql/30_rpc/admin/rpc_admin_system_config_save_v1.sql
Normal file
37
docs/sql/30_rpc/admin/rpc_admin_system_config_save_v1.sql
Normal 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;
|
||||||
|
$$;
|
||||||
103
docs/sql/30_rpc/finance/rpc_admin_extract_list_v1.sql
Normal file
103
docs/sql/30_rpc/finance/rpc_admin_extract_list_v1.sql
Normal 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 '管理员提现申请列表分页查询';
|
||||||
78
docs/sql/30_rpc/finance/rpc_admin_extract_review_v1.sql
Normal file
78
docs/sql/30_rpc/finance/rpc_admin_extract_review_v1.sql
Normal 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:通过时扣款)';
|
||||||
64
docs/sql/30_rpc/finance/rpc_admin_recharge_audit_v1.sql
Normal file
64
docs/sql/30_rpc/finance/rpc_admin_recharge_audit_v1.sql
Normal 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 '管理员人工审计/补单(更新用户余额并生成流水)';
|
||||||
92
docs/sql/30_rpc/finance/rpc_admin_recharge_list_v1.sql
Normal file
92
docs/sql/30_rpc/finance/rpc_admin_recharge_list_v1.sql
Normal 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 '管理员充值记录列表分页查询';
|
||||||
99
docs/sql/30_rpc/finance/rpc_admin_user_bill_list_v1.sql
Normal file
99
docs/sql/30_rpc/finance/rpc_admin_user_bill_list_v1.sql
Normal 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 '管理员资金流水列表分页查询';
|
||||||
68
docs/sql/30_rpc/order/rpc_admin_cashier_order_list_v1.sql
Normal file
68
docs/sql/30_rpc/order/rpc_admin_cashier_order_list_v1.sql
Normal 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;
|
||||||
|
$$;
|
||||||
87
docs/sql/30_rpc/order/rpc_admin_refund_order_list_v1.sql
Normal file
87
docs/sql/30_rpc/order/rpc_admin_refund_order_list_v1.sql
Normal 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;
|
||||||
|
$$;
|
||||||
80
docs/sql/30_rpc/order/rpc_admin_write_off_record_list_v1.sql
Normal file
80
docs/sql/30_rpc/order/rpc_admin_write_off_record_list_v1.sql
Normal 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;
|
||||||
|
$$;
|
||||||
46
docs/sql/30_rpc/product/rpc_admin_category_delete_v1.sql
Normal file
46
docs/sql/30_rpc/product/rpc_admin_category_delete_v1.sql
Normal 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;
|
||||||
|
$$;
|
||||||
105
docs/sql/30_rpc/product/rpc_admin_product_analytics_v1.sql
Normal file
105
docs/sql/30_rpc/product/rpc_admin_product_analytics_v1.sql
Normal 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;
|
||||||
|
$$;
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 操作文档:Admin 商品模块标准化实施
|
||||||
|
|
||||||
|
- **日期**:2026-02-06
|
||||||
|
- **作用域**:`admin` / `product`
|
||||||
|
- **实施人**:Cascade (AI Assistant)
|
||||||
|
|
||||||
|
## 1. 摘要
|
||||||
|
按照 `AGENT_PROJECT_SPEC.md` 规范,完成了 Admin 商品模块从数据库 RPC 到 Service 层,再到前端页面的全链路标准化改造。
|
||||||
|
|
||||||
|
## 2. 动机
|
||||||
|
- 统一商品模块数据访问口径,消除页面 Mock 数据。
|
||||||
|
- 增强数据库安全性,所有特权操作均通过 `SECURITY DEFINER` RPC 并包含角色校验。
|
||||||
|
- 修复分类层级变动时 `path` 与 `level` 字段不同步的潜在风险。
|
||||||
|
|
||||||
|
## 3. 影响范围
|
||||||
|
- **数据库**:新增/更新了 `rpc_admin_product_*` 和 `rpc_admin_category_*` 系列函数。
|
||||||
|
- **服务层**:新增 `services/admin/productService.uts` 和 `services/admin/productCategoryService.uts`。
|
||||||
|
- **前端页面**:重构了 `product-management/index.uvue` 和 `classification/index.uvue`。
|
||||||
|
|
||||||
|
## 4. 变更清单
|
||||||
|
|
||||||
|
### 4.1 数据库 RPC (docs/sql/30_rpc/product/)
|
||||||
|
- `rpc_admin_product_list_v1.sql`: 标准化分页查询,对齐 `JSONB` 返回结构。
|
||||||
|
- `rpc_admin_product_update_status_v1.sql`: 统一处理上下架与回收站逻辑。
|
||||||
|
- `rpc_admin_category_list_v1.sql`: 适配 `ml_categories` 权威字段。
|
||||||
|
- `rpc_admin_category_create_v1.sql`: 自动维护层级路径。
|
||||||
|
- `rpc_admin_category_update_v1.sql`: **核心增强**,支持子树 `path` 与 `level` 的级联更新,并具备递归防循环引用校验。
|
||||||
|
- `rpc_admin_category_delete_v1.sql`: 实现“有子项禁止删除”的安全策略。
|
||||||
|
|
||||||
|
### 4.2 服务层 (services/admin/)
|
||||||
|
- `productService.uts`: 封装商品列表与状态变更接口。
|
||||||
|
- `productCategoryService.uts`: 封装分类列表与 CRUD 接口。
|
||||||
|
|
||||||
|
### 4.3 前端重构
|
||||||
|
- **商品管理**:接入真实数据流,支持按名称、状态搜索,支持实时上下架切换。
|
||||||
|
- **商品分类**:接入真实树形数据,支持完整的 CRUD 操作与状态开关。
|
||||||
|
|
||||||
|
## 5. 安全与权限验证
|
||||||
|
- **RPC 安全**:所有函数均声明为 `SECURITY DEFINER`,并固定 `search_path = public`。
|
||||||
|
- **角色守卫**:函数入口显式校验 `role IN ('admin', 'analytics')`。
|
||||||
|
- **数据隔离**:仅返回 UI 渲染必要的最小字段集。
|
||||||
|
|
||||||
|
## 6. 回滚方案
|
||||||
|
- **SQL**:执行 `DROP FUNCTION IF EXISTS public.rpc_admin_...`。
|
||||||
|
- **代码**:通过 Git 回退 `pages/mall/admin/product/` 相关目录的变更。
|
||||||
|
|
||||||
|
## 7. 验证方式
|
||||||
|
1. 登录 Admin 账号,进入“商品管理”,验证列表分页与搜索是否正常。
|
||||||
|
2. 切换商品“上架/下架”开关,刷新页面确认状态持久化。
|
||||||
|
3. 进入“商品分类”,尝试添加子分类并移动其父级,通过数据库查询确认其 `path` 已级联修正。
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<view class="icon-box blue"><text class="icon">🕒</text></view>
|
<view class="icon-box blue"><text class="icon">🕒</text></view>
|
||||||
<view class="item-info">
|
<view class="item-info">
|
||||||
<text class="item-label">营业额</text>
|
<text class="item-label">营业额</text>
|
||||||
<text class="item-value">442753.70</text>
|
<text class="item-value">{{ stats.revenue }}</text>
|
||||||
<view class="trend-row">
|
<view class="trend-row">
|
||||||
<text class="trend-label">环比增长:</text>
|
<text class="trend-label">环比增长:</text>
|
||||||
<text class="trend-value up">44275370% ▲</text>
|
<text class="trend-value up">44275370% ▲</text>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<view class="icon-box green"><text class="icon">¥</text></view>
|
<view class="icon-box green"><text class="icon">¥</text></view>
|
||||||
<view class="item-info">
|
<view class="item-info">
|
||||||
<text class="item-label">商品支付金额</text>
|
<text class="item-label">商品支付金额</text>
|
||||||
<text class="item-value">434693.52</text>
|
<text class="item-value">{{ stats.payAmount }}</text>
|
||||||
<view class="trend-row">
|
<view class="trend-row">
|
||||||
<text class="trend-label">环比增长:</text>
|
<text class="trend-label">环比增长:</text>
|
||||||
<text class="trend-value up">43469352% ▲</text>
|
<text class="trend-value up">43469352% ▲</text>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<view class="icon-box orange"><text class="icon">🔒</text></view>
|
<view class="icon-box orange"><text class="icon">🔒</text></view>
|
||||||
<view class="item-info">
|
<view class="item-info">
|
||||||
<text class="item-label">购买会员金额</text>
|
<text class="item-label">购买会员金额</text>
|
||||||
<text class="item-value">8059.18</text>
|
<text class="item-value">{{ stats.memberAmount }}</text>
|
||||||
<view class="trend-row">
|
<view class="trend-row">
|
||||||
<text class="trend-label">环比增长:</text>
|
<text class="trend-label">环比增长:</text>
|
||||||
<text class="trend-value up">805918% ▲</text>
|
<text class="trend-value up">805918% ▲</text>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<view class="icon-box purple"><text class="icon">💰</text></view>
|
<view class="icon-box purple"><text class="icon">💰</text></view>
|
||||||
<view class="item-info">
|
<view class="item-info">
|
||||||
<text class="item-label">充值金额</text>
|
<text class="item-label">充值金额</text>
|
||||||
<text class="item-value">0.00</text>
|
<text class="item-value">{{ stats.rechargeAmount }}</text>
|
||||||
<view class="trend-row">
|
<view class="trend-row">
|
||||||
<text class="trend-label">环比增长:</text>
|
<text class="trend-label">环比增长:</text>
|
||||||
<text class="trend-value">0% -</text>
|
<text class="trend-value">0% -</text>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<view class="icon-box cyan"><text class="icon">🛒</text></view>
|
<view class="icon-box cyan"><text class="icon">🛒</text></view>
|
||||||
<view class="item-info">
|
<view class="item-info">
|
||||||
<text class="item-label">线下收银金额</text>
|
<text class="item-label">线下收银金额</text>
|
||||||
<text class="item-value">1</text>
|
<text class="item-value">{{ stats.offlineAmount }}</text>
|
||||||
<view class="trend-row">
|
<view class="trend-row">
|
||||||
<text class="trend-label">环比增长:</text>
|
<text class="trend-label">环比增长:</text>
|
||||||
<text class="trend-value up">100% ▲</text>
|
<text class="trend-value up">100% ▲</text>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<view class="icon-box light-green"><text class="icon">↘</text></view>
|
<view class="icon-box light-green"><text class="icon">↘</text></view>
|
||||||
<view class="item-info">
|
<view class="item-info">
|
||||||
<text class="item-label">支出金额</text>
|
<text class="item-label">支出金额</text>
|
||||||
<text class="item-value">442752.69</text>
|
<text class="item-value">{{ stats.expenditure }}</text>
|
||||||
<view class="trend-row">
|
<view class="trend-row">
|
||||||
<text class="trend-label">环比增长:</text>
|
<text class="trend-label">环比增长:</text>
|
||||||
<text class="trend-value up">44275269% ▲</text>
|
<text class="trend-value up">44275269% ▲</text>
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
<view class="icon-box gold"><text class="icon">💳</text></view>
|
<view class="icon-box gold"><text class="icon">💳</text></view>
|
||||||
<view class="item-info">
|
<view class="item-info">
|
||||||
<text class="item-label">余额支付金额</text>
|
<text class="item-label">余额支付金额</text>
|
||||||
<text class="item-value">442752.69</text>
|
<text class="item-value">{{ stats.balancePay }}</text>
|
||||||
<view class="trend-row">
|
<view class="trend-row">
|
||||||
<text class="trend-label">环比增长:</text>
|
<text class="trend-label">环比增长:</text>
|
||||||
<text class="trend-value up">5293.00% ▲</text>
|
<text class="trend-value up">5293.00% ▲</text>
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
<view class="icon-box red-purple"><text class="icon">%</text></view>
|
<view class="icon-box red-purple"><text class="icon">%</text></view>
|
||||||
<view class="item-info">
|
<view class="item-info">
|
||||||
<text class="item-label">支付佣金金额</text>
|
<text class="item-label">支付佣金金额</text>
|
||||||
<text class="item-value">0.00</text>
|
<text class="item-value">{{ stats.commissionPay }}</text>
|
||||||
<view class="trend-row">
|
<view class="trend-row">
|
||||||
<text class="trend-label">环比增长:</text>
|
<text class="trend-label">环比增长:</text>
|
||||||
<text class="trend-value">0% -</text>
|
<text class="trend-value">0% -</text>
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
<view class="icon-box blue-gray"><text class="icon">📦</text></view>
|
<view class="icon-box blue-gray"><text class="icon">📦</text></view>
|
||||||
<view class="item-info">
|
<view class="item-info">
|
||||||
<text class="item-label">商品退款金额</text>
|
<text class="item-label">商品退款金额</text>
|
||||||
<text class="item-value">0.00</text>
|
<text class="item-value">{{ stats.refundAmount }}</text>
|
||||||
<view class="trend-row">
|
<view class="trend-row">
|
||||||
<text class="trend-label">环比增长:</text>
|
<text class="trend-label">环比增长:</text>
|
||||||
<text class="trend-value">0% -</text>
|
<text class="trend-value">0% -</text>
|
||||||
@@ -281,42 +281,69 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||||
|
import { fetchFinanceOverview } from '@/services/admin/financeService.uts'
|
||||||
|
import { rpcOrNull } from '@/services/analytics/rpc.uts'
|
||||||
|
|
||||||
const dateOptions = ['今天', '昨天', '最近7天', '最近30天', '本月', '本年']
|
const dateOptions = ['今天', '昨天', '最近7天', '最近30天', '本月', '本年']
|
||||||
const activeDateTab = ref(0)
|
const activeDateTab = ref(0)
|
||||||
|
|
||||||
|
// 响应式统计数据
|
||||||
|
const stats = reactive({
|
||||||
|
revenue: '0.00', // 营业额
|
||||||
|
payAmount: '0.00', // 商品支付金额
|
||||||
|
memberAmount: '0.00', // 购买会员金额
|
||||||
|
rechargeAmount: '0.00', // 充值金额
|
||||||
|
offlineAmount: '0.00', // 线下收银金额
|
||||||
|
expenditure: '0.00', // 支出金额
|
||||||
|
balancePay: '0.00', // 余额支付金额
|
||||||
|
commissionPay: '0.00', // 支付佣金金额
|
||||||
|
refundAmount: '0.00', // 商品退款金额
|
||||||
|
|
||||||
|
// 环比数据 (示例暂留)
|
||||||
|
revenueTrend: '0%',
|
||||||
|
rechargeTrend: '0%'
|
||||||
|
})
|
||||||
|
|
||||||
const orderAmountOption = ref<any>(null)
|
const orderAmountOption = ref<any>(null)
|
||||||
const overviewTrendOption = ref<any>(null)
|
const overviewTrendOption = ref<any>(null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具函数:将 UTS 对象转换为纯 JavaScript 对象
|
* 加载统计数据
|
||||||
* 确保 ECharts 在 renderjs 中能正确接收数据
|
|
||||||
*/
|
*/
|
||||||
function toPlainObject(obj : any) : any {
|
async function loadData() {
|
||||||
if (obj == null) return null
|
// TODO: 根据 activeDateTab 计算具体的 startTime 和 endTime
|
||||||
if (typeof obj !== 'object') return obj
|
const startTime = '2026-01-01T00:00:00Z'
|
||||||
if (Array.isArray(obj)) {
|
const endTime = '2026-12-31T23:59:59Z'
|
||||||
return (obj as Array<any>).map((item : any) : any => toPlainObject(item))
|
|
||||||
}
|
try {
|
||||||
const plain : Record<string, any> = {}
|
// 1. 获取财务概况 (充值、提现等)
|
||||||
const keys = Object.keys(obj as Record<string, any>)
|
const financeRes = await fetchFinanceOverview(startTime, endTime)
|
||||||
for (let i = 0; i < keys.length; i++) {
|
if (financeRes != null) {
|
||||||
const key = keys[i]
|
stats.rechargeAmount = financeRes.recharge_amount.toFixed(2)
|
||||||
if (key.startsWith('_') || key == 'toJSON') continue
|
// 支出金额暂以提现成功金额为例
|
||||||
const value = (obj as Record<string, any>)[key]
|
stats.expenditure = financeRes.extract_amount.toFixed(2)
|
||||||
if (typeof value == 'function') continue
|
|
||||||
if (value != null && typeof value == 'object' && !Array.isArray(value)) {
|
|
||||||
plain[key] = toPlainObject(value)
|
|
||||||
} else {
|
|
||||||
plain[key] = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 获取订单统计 (营业额、退款等)
|
||||||
|
const orderRes = await rpcOrNull('rpc_admin_order_stats', {
|
||||||
|
p_start_time: startTime,
|
||||||
|
p_end_time: endTime
|
||||||
|
} as UTSJSONObject)
|
||||||
|
|
||||||
|
if (orderRes != null) {
|
||||||
|
stats.revenue = ((orderRes as any).total_amount ?? 0).toFixed(2)
|
||||||
|
stats.payAmount = stats.revenue // 简单处理
|
||||||
|
stats.refundAmount = ((orderRes as any).refund_amount ?? 0).toFixed(2)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load transaction stats:', e)
|
||||||
}
|
}
|
||||||
return plain
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
// 延迟初始化图表确保容器就位
|
// 延迟初始化图表确保容器就位
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
initCharts()
|
initCharts()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<view class="custom-icon icon-order"></view>
|
<view class="custom-icon icon-order"></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-info">
|
<view class="stat-info">
|
||||||
<text class="stat-value">209</text>
|
<text class="stat-value">{{ orderStats?.order_count ?? 0 }}</text>
|
||||||
<text class="stat-desc">订单量</text>
|
<text class="stat-desc">订单量</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<view class="custom-icon icon-money"></view>
|
<view class="custom-icon icon-money"></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-info">
|
<view class="stat-info">
|
||||||
<text class="stat-value">443254.62</text>
|
<text class="stat-value">{{ orderStats?.total_amount?.toFixed(2) ?? '0.00' }}</text>
|
||||||
<text class="stat-desc">订单销售额</text>
|
<text class="stat-desc">订单销售额</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<view class="custom-icon icon-refund"></view>
|
<view class="custom-icon icon-refund"></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-info">
|
<view class="stat-info">
|
||||||
<text class="stat-value">0</text>
|
<text class="stat-value">{{ orderStats?.refund_count ?? 0 }}</text>
|
||||||
<text class="stat-desc">退款订单数</text>
|
<text class="stat-desc">退款订单数</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
<view class="custom-icon icon-refund-money"></view>
|
<view class="custom-icon icon-refund-money"></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-info">
|
<view class="stat-info">
|
||||||
<text class="stat-value">0</text>
|
<text class="stat-value">{{ orderStats?.refund_amount?.toFixed(2) ?? '0.00' }}</text>
|
||||||
<text class="stat-desc">退款金额</text>
|
<text class="stat-desc">退款金额</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -149,59 +149,99 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import {
|
||||||
|
fetchAdminCategoryList,
|
||||||
|
createAdminCategory,
|
||||||
|
updateAdminCategory,
|
||||||
|
deleteAdminCategory,
|
||||||
|
type AdminCategory
|
||||||
|
} from '@/services/admin/productCategoryService.uts'
|
||||||
|
|
||||||
interface CateItem {
|
interface CateItem {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
sort: number;
|
sort: number;
|
||||||
status: boolean;
|
status: boolean;
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
children?: CateItem[];
|
children?: CateItem[];
|
||||||
parentId?: number;
|
parentId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = reactive<CateItem[]>([
|
const list = ref<Array<CateItem>>([])
|
||||||
{
|
|
||||||
id: 100, name: '家用电器', icon: '/static/logo.png', sort: 1, status: true, expanded: true,
|
|
||||||
children: [
|
|
||||||
{ id: 101, name: '电视机', icon: '/static/logo.png', sort: 1, status: true, parentId: 100 },
|
|
||||||
{ id: 102, name: '电冰箱', icon: '/static/logo.png', sort: 2, status: true, parentId: 100 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 200, name: '手机数码', icon: '/static/logo.png', sort: 2, status: true, expanded: false,
|
|
||||||
children: [
|
|
||||||
{ id: 201, name: '手机', icon: '/static/logo.png', sort: 1, status: true, parentId: 200 },
|
|
||||||
{ id: 202, name: '耳机', icon: '/static/logo.png', sort: 2, status: true, parentId: 200 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const showDrawerMask = ref(false)
|
const showDrawerMask = ref(false)
|
||||||
const showDrawer = ref(false)
|
const showDrawer = ref(false)
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
|
const editingId = ref<string | null>(null)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
|
parentId: null as string | null,
|
||||||
parentName: '',
|
parentName: '',
|
||||||
sort: 0,
|
sort: 0,
|
||||||
status: true
|
status: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadList()
|
||||||
|
})
|
||||||
|
|
||||||
|
function buildTree(items: Array<AdminCategory>): Array<CateItem> {
|
||||||
|
const map: Record<string, CateItem> = {}
|
||||||
|
const roots: Array<CateItem> = []
|
||||||
|
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
const c = items[i]
|
||||||
|
map[c.id] = {
|
||||||
|
id: c.id,
|
||||||
|
name: c.name,
|
||||||
|
icon: c.icon ?? '',
|
||||||
|
sort: c.sort ?? 0,
|
||||||
|
status: c.is_active === true,
|
||||||
|
expanded: false,
|
||||||
|
children: [],
|
||||||
|
parentId: c.parent_id ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = Object.keys(map)
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
const id = ids[i]
|
||||||
|
const node = map[id]
|
||||||
|
if (node.parentId != null && map[node.parentId] != null) {
|
||||||
|
map[node.parentId].children = map[node.parentId].children ?? []
|
||||||
|
map[node.parentId].children!.push(node)
|
||||||
|
} else {
|
||||||
|
roots.push(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return roots
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadList() {
|
||||||
|
const items = await fetchAdminCategoryList({})
|
||||||
|
list.value = buildTree(items)
|
||||||
|
}
|
||||||
|
|
||||||
function openDrawer(item: CateItem | null = null) {
|
function openDrawer(item: CateItem | null = null) {
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
isEdit.value = true
|
isEdit.value = true
|
||||||
|
editingId.value = item.id
|
||||||
form.name = item.name
|
form.name = item.name
|
||||||
form.sort = item.sort
|
form.sort = item.sort
|
||||||
form.status = item.status
|
form.status = item.status
|
||||||
|
form.parentId = item.parentId ?? null
|
||||||
form.parentName = item.parentId != null ? '子分类' : '顶级分类'
|
form.parentName = item.parentId != null ? '子分类' : '顶级分类'
|
||||||
} else {
|
} else {
|
||||||
isEdit.value = false
|
isEdit.value = false
|
||||||
|
editingId.value = null
|
||||||
form.name = ''
|
form.name = ''
|
||||||
form.sort = 0
|
form.sort = 0
|
||||||
form.status = true
|
form.status = true
|
||||||
|
form.parentId = null
|
||||||
form.parentName = '顶级分类'
|
form.parentName = '顶级分类'
|
||||||
}
|
}
|
||||||
showDrawerMask.value = true
|
showDrawerMask.value = true
|
||||||
@@ -217,22 +257,49 @@ showDrawerMask.value = false
|
|||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCate() {
|
async function saveCate() {
|
||||||
|
if (isEdit.value && editingId.value != null) {
|
||||||
|
await updateAdminCategory({
|
||||||
|
id: editingId.value,
|
||||||
|
parentId: form.parentId,
|
||||||
|
name: form.name,
|
||||||
|
sortOrder: form.sort,
|
||||||
|
isActive: form.status
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createAdminCategory({
|
||||||
|
parentId: form.parentId,
|
||||||
|
name: form.name,
|
||||||
|
sortOrder: form.sort,
|
||||||
|
isActive: form.status
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||||
closeDrawer()
|
closeDrawer()
|
||||||
|
loadList()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleStatus(item: CateItem) {
|
async function toggleStatus(item: CateItem) {
|
||||||
|
await updateAdminCategory({
|
||||||
|
id: item.id,
|
||||||
|
parentId: item.parentId,
|
||||||
|
name: item.name,
|
||||||
|
sortOrder: item.sort,
|
||||||
|
isActive: !item.status
|
||||||
|
})
|
||||||
item.status = !item.status
|
item.status = !item.status
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteItem(item: CateItem) {
|
async function deleteItem(item: CateItem) {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: '确定删除该分类吗?',
|
content: '确定删除该分类吗?',
|
||||||
success: (res) => {
|
success: async (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
uni.showToast({ title: '已模拟删除', icon: 'none' })
|
await deleteAdminCategory(item.id)
|
||||||
|
uni.showToast({ title: '删除成功', icon: 'success' })
|
||||||
|
loadList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -160,91 +160,97 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
|
||||||
|
import { fetchAdminProductPage, updateAdminProductStatus, type AdminProduct } from '@/services/admin/productService.uts'
|
||||||
|
|
||||||
const total = ref(49)
|
const total = ref(0)
|
||||||
const activeStatus = ref('selling')
|
const page = ref(1)
|
||||||
const activeDropdownId = ref<number | null>(null)
|
const pageSize = ref(10)
|
||||||
|
const activeStatus = ref<number | null>(1) // 1:出售中
|
||||||
|
const activeDropdownId = ref<string | null>(null)
|
||||||
|
const productList = ref<Array<AdminProduct>>([])
|
||||||
|
const searchName = ref('')
|
||||||
|
|
||||||
const statusTabs = ref([
|
const statusTabs = ref([
|
||||||
{ key: 'selling', label: '出售中的商品', count: 49 },
|
{ key: 1, label: '出售中的商品', count: 0 },
|
||||||
{ key: 'warehouse', label: '仓库中的商品', count: 4 },
|
{ key: 2, label: '仓库中的商品', count: 0 },
|
||||||
{ key: 'soldout', label: '已经售罄商品', count: 11 },
|
{ key: 3, label: '草稿箱', count: 0 },
|
||||||
{ key: 'alarm', label: '警戒库存商品', count: 27 },
|
{ key: 4, label: '回收站', count: 0 },
|
||||||
{ key: 'recycle', label: '回收站的商品', count: 176 },
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const productList = ref([
|
onMounted(() => {
|
||||||
{
|
loadData()
|
||||||
id: 963,
|
})
|
||||||
image: 'https://img1.baidu.com/it/u=254065646,3100346083&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
|
||||||
name: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060',
|
|
||||||
activities: ['kj', 'pt'],
|
|
||||||
typeName: '普通商品',
|
|
||||||
price: '0.01',
|
|
||||||
sales: 639,
|
|
||||||
stock: 1602,
|
|
||||||
sort: 9999,
|
|
||||||
status: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 108,
|
|
||||||
image: 'https://img2.baidu.com/it/u=3033501986,2204481084&fm=253&fmt=auto&app=138&f=JPEG?w=569&h=500',
|
|
||||||
name: 'FOMIX 蛋壳椅 进口头层牛皮橙色单人沙发椅Egg chair设计师师单椅单沙头层牛皮/单椅',
|
|
||||||
activities: ['pt', 'ms'],
|
|
||||||
typeName: '普通商品',
|
|
||||||
price: '7580.00',
|
|
||||||
sales: 14,
|
|
||||||
stock: 16638,
|
|
||||||
sort: 9999,
|
|
||||||
status: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 48,
|
|
||||||
image: 'https://img0.baidu.com/it/u=1762118431,3101886131&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
|
|
||||||
name: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇墨水蓝/传奇墨水蓝/白 XL',
|
|
||||||
activities: ['kj', 'pt', 'ms'],
|
|
||||||
typeName: '普通商品',
|
|
||||||
price: '100.00',
|
|
||||||
sales: 841,
|
|
||||||
stock: 2318,
|
|
||||||
sort: 9998,
|
|
||||||
status: 1
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
function getActivityName(tag: string): string {
|
async function loadData() {
|
||||||
if (tag === 'kj') return '砍价'
|
const res = await fetchAdminProductPage(page.value, pageSize.value, {
|
||||||
if (tag === 'pt') return '拼团'
|
name: searchName.value,
|
||||||
if (tag === 'ms') return '秒杀'
|
status: activeStatus.value ?? undefined
|
||||||
return tag
|
})
|
||||||
|
productList.value = res.items
|
||||||
|
total.value = res.total
|
||||||
}
|
}
|
||||||
|
|
||||||
function goEdit(id: number | null) {
|
function handleSearch() {
|
||||||
openRoute('product_edit')
|
page.value = 1
|
||||||
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
function goReviews(id: number) {
|
function handleReset() {
|
||||||
openRoute('product_productReply')
|
searchName.value = ''
|
||||||
|
page.value = 1
|
||||||
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
function goMemberPrice(id: number) {
|
async function toggleStatus(item: AdminProduct) {
|
||||||
openRoute('product_member_price')
|
const newStatus = item.status === 1 ? 2 : 1
|
||||||
|
const ok = await updateAdminProductStatus(item.id, newStatus)
|
||||||
|
if (ok) {
|
||||||
|
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||||
|
loadData()
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveToRecycle(id: number) {
|
async function moveToRecycle(item: AdminProduct) {
|
||||||
const action = activeStatus.value === 'recycle' ? '恢复' : '移到回收站';
|
const isDelete = activeStatus.value !== 4
|
||||||
|
const targetStatus = isDelete ? 4 : 2 // 移到回收站或恢复到下架
|
||||||
|
|
||||||
|
const action = isDelete ? '移到回收站' : '恢复'
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
content: `确认要将该商品${action}吗?`,
|
content: `确认要将该商品${action}吗?`,
|
||||||
success: (res) => {
|
success: async (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
uni.showToast({ title: '操作成功', icon: 'success' });
|
const ok = await updateAdminProductStatus(item.id, targetStatus)
|
||||||
|
if (ok) {
|
||||||
|
uni.showToast({ title: '操作成功', icon: 'success' })
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeStatus(key: number) {
|
||||||
|
activeStatus.value = key
|
||||||
|
page.value = 1
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function goEdit(id: string | null) {
|
||||||
|
openRoute('product_edit')
|
||||||
|
}
|
||||||
|
|
||||||
|
function goReviews(id: string) {
|
||||||
|
openRoute('product_productReply')
|
||||||
|
}
|
||||||
|
|
||||||
|
function goMemberPrice(id: string) {
|
||||||
|
openRoute('product_member_price')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
151
services/admin/financeService.uts
Normal file
151
services/admin/financeService.uts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { rpcOrNull, rpcOrValue, rpcOrEmptyArray } from '@/services/analytics/rpc.uts'
|
||||||
|
import { FinanceOverview, ExtractRecord, UserBillRecord, PagedResult } from '@/types/admin/finance.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 财务核心服务
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取财务概况统计
|
||||||
|
*/
|
||||||
|
export async function fetchFinanceOverview(startTime : string, endTime : string) : Promise<FinanceOverview | null> {
|
||||||
|
const res = await rpcOrNull('rpc_admin_finance_overview', {
|
||||||
|
p_start_time: startTime,
|
||||||
|
p_end_time: endTime
|
||||||
|
} as UTSJSONObject)
|
||||||
|
|
||||||
|
if (res == null) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
recharge_amount: (res as any).recharge_amount ?? 0,
|
||||||
|
recharge_count: (res as any).recharge_count ?? 0,
|
||||||
|
extract_amount: (res as any).extract_amount ?? 0,
|
||||||
|
extract_count: (res as any).extract_count ?? 0,
|
||||||
|
total_user_balance: (res as any).total_user_balance ?? 0,
|
||||||
|
total_user_brokerage: (res as any).total_user_brokerage ?? 0
|
||||||
|
} as FinanceOverview
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核提现申请
|
||||||
|
*/
|
||||||
|
export async function reviewExtract(extractId : string, status : number, refusalReason : string | null = null) : Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await rpcOrValue('rpc_admin_extract_review', {
|
||||||
|
p_extract_id: extractId,
|
||||||
|
p_status: status,
|
||||||
|
p_refusal_reason: refusalReason
|
||||||
|
} as UTSJSONObject)
|
||||||
|
return true
|
||||||
|
} catch (e : any) {
|
||||||
|
console.error('reviewExtract failed:', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 充值补单审计
|
||||||
|
*/
|
||||||
|
export async function auditRecharge(rechargeId : string, mark : string | null = null) : Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await rpcOrValue('rpc_admin_recharge_audit', {
|
||||||
|
p_recharge_id: rechargeId,
|
||||||
|
p_mark: mark
|
||||||
|
} as UTSJSONObject)
|
||||||
|
return true
|
||||||
|
} catch (e : any) {
|
||||||
|
console.error('auditRecharge failed:', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提现列表
|
||||||
|
*/
|
||||||
|
export async function fetchExtractList(
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
status: number | null = null,
|
||||||
|
startTime: string | null = null,
|
||||||
|
endTime: string | null = null,
|
||||||
|
search: string | null = null
|
||||||
|
): Promise<PagedResult<ExtractRecord>> {
|
||||||
|
const res = await rpcOrNull('rpc_admin_extract_list', {
|
||||||
|
p_page: page,
|
||||||
|
p_page_size: pageSize,
|
||||||
|
p_status: status,
|
||||||
|
p_start_time: startTime,
|
||||||
|
p_end_time: endTime,
|
||||||
|
p_search: search
|
||||||
|
} as UTSJSONObject)
|
||||||
|
|
||||||
|
if (res == null) return { total: 0, items: [] as Array<ExtractRecord> }
|
||||||
|
|
||||||
|
const data = res as any
|
||||||
|
return {
|
||||||
|
total: data.total ?? 0,
|
||||||
|
items: (data.items as Array<ExtractRecord>) ?? ([] as Array<ExtractRecord>)
|
||||||
|
} as PagedResult<ExtractRecord>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取充值记录列表
|
||||||
|
*/
|
||||||
|
export async function fetchRechargeList(
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
paid: number | null = null,
|
||||||
|
startTime: string | null = null,
|
||||||
|
endTime: string | null = null,
|
||||||
|
search: string | null = null
|
||||||
|
): Promise<PagedResult<any>> {
|
||||||
|
const res = await rpcOrNull('rpc_admin_recharge_list', {
|
||||||
|
p_page: page,
|
||||||
|
p_page_size: pageSize,
|
||||||
|
p_paid: paid,
|
||||||
|
p_start_time: startTime,
|
||||||
|
p_end_time: endTime,
|
||||||
|
p_search: search
|
||||||
|
} as UTSJSONObject)
|
||||||
|
|
||||||
|
if (res == null) return { total: 0, items: [] as Array<any> }
|
||||||
|
|
||||||
|
const data = res as any
|
||||||
|
return {
|
||||||
|
total: data.total ?? 0,
|
||||||
|
items: (data.items as Array<any>) ?? ([] as Array<any>)
|
||||||
|
} as PagedResult<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资金流水列表
|
||||||
|
*/
|
||||||
|
export async function fetchUserBillList(
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
category: string | null = null,
|
||||||
|
type: string | null = null,
|
||||||
|
pm: number | null = null,
|
||||||
|
startTime: string | null = null,
|
||||||
|
endTime: string | null = null,
|
||||||
|
search: string | null = null
|
||||||
|
): Promise<PagedResult<UserBillRecord>> {
|
||||||
|
const res = await rpcOrNull('rpc_admin_user_bill_list', {
|
||||||
|
p_page: page,
|
||||||
|
p_page_size: pageSize,
|
||||||
|
p_category: category,
|
||||||
|
p_type: type,
|
||||||
|
p_pm: pm,
|
||||||
|
p_start_time: startTime,
|
||||||
|
p_end_time: endTime,
|
||||||
|
p_search: search
|
||||||
|
} as UTSJSONObject)
|
||||||
|
|
||||||
|
if (res == null) return { total: 0, items: [] as Array<UserBillRecord> }
|
||||||
|
|
||||||
|
const data = res as any
|
||||||
|
return {
|
||||||
|
total: data.total ?? 0,
|
||||||
|
items: (data.items as Array<UserBillRecord>) ?? ([] as Array<UserBillRecord>)
|
||||||
|
} as PagedResult<UserBillRecord>
|
||||||
|
}
|
||||||
116
services/admin/productCategoryService.uts
Normal file
116
services/admin/productCategoryService.uts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { rpcOrNull, rpcOrValue } from '@/services/analytics/rpc.uts'
|
||||||
|
|
||||||
|
export type AdminCategory = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
parent_id: string | null
|
||||||
|
icon: string | null
|
||||||
|
sort: number
|
||||||
|
is_active: boolean
|
||||||
|
level: number
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分类列表
|
||||||
|
*/
|
||||||
|
export async function fetchAdminCategoryList(filters: {
|
||||||
|
name?: string,
|
||||||
|
isActive?: boolean
|
||||||
|
}): Promise<Array<AdminCategory>> {
|
||||||
|
const res = await rpcOrNull('rpc_admin_category_list', {
|
||||||
|
p_name: filters.name ?? null,
|
||||||
|
p_is_active: filters.isActive ?? null
|
||||||
|
} as UTSJSONObject)
|
||||||
|
|
||||||
|
if (res == null) return [] as Array<AdminCategory>
|
||||||
|
|
||||||
|
const anyItems = (res as any).items
|
||||||
|
return Array.isArray(anyItems) ? anyItems : [] as Array<AdminCategory>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分类
|
||||||
|
*/
|
||||||
|
export async function createAdminCategory(payload: {
|
||||||
|
parentId?: string | null,
|
||||||
|
name: string,
|
||||||
|
slug?: string,
|
||||||
|
description?: string,
|
||||||
|
iconUrl?: string,
|
||||||
|
bannerUrl?: string,
|
||||||
|
sortOrder?: number,
|
||||||
|
isActive?: boolean
|
||||||
|
}): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const id = await rpcOrValue('rpc_admin_category_create', {
|
||||||
|
p_parent_id: payload.parentId ?? null,
|
||||||
|
p_name: payload.name,
|
||||||
|
p_slug: payload.slug ?? null,
|
||||||
|
p_description: payload.description ?? null,
|
||||||
|
p_icon_url: payload.iconUrl ?? null,
|
||||||
|
p_banner_url: payload.bannerUrl ?? null,
|
||||||
|
p_sort_order: payload.sortOrder ?? 0,
|
||||||
|
p_is_active: payload.isActive ?? true
|
||||||
|
} as UTSJSONObject)
|
||||||
|
return typeof id === 'string' ? id as string : null
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('创建分类失败:', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新分类
|
||||||
|
*/
|
||||||
|
export async function updateAdminCategory(payload: {
|
||||||
|
id: string,
|
||||||
|
parentId?: string | null,
|
||||||
|
name: string,
|
||||||
|
slug?: string,
|
||||||
|
description?: string,
|
||||||
|
iconUrl?: string,
|
||||||
|
bannerUrl?: string,
|
||||||
|
sortOrder?: number,
|
||||||
|
isActive?: boolean
|
||||||
|
}): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const ok = await rpcOrValue('rpc_admin_category_update', {
|
||||||
|
p_id: payload.id,
|
||||||
|
p_parent_id: payload.parentId ?? null,
|
||||||
|
p_name: payload.name,
|
||||||
|
p_slug: payload.slug ?? null,
|
||||||
|
p_description: payload.description ?? null,
|
||||||
|
p_icon_url: payload.iconUrl ?? null,
|
||||||
|
p_banner_url: payload.bannerUrl ?? null,
|
||||||
|
p_sort_order: payload.sortOrder ?? 0,
|
||||||
|
p_is_active: payload.isActive ?? true
|
||||||
|
} as UTSJSONObject)
|
||||||
|
return ok === true
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('更新分类失败:', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分类
|
||||||
|
*/
|
||||||
|
export async function deleteAdminCategory(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const ok = await rpcOrValue('rpc_admin_category_delete', {
|
||||||
|
p_id: id
|
||||||
|
} as UTSJSONObject)
|
||||||
|
return ok === true
|
||||||
|
} catch (e: any) {
|
||||||
|
const error = e as UTSJSONObject
|
||||||
|
const message = error.getString('message') ?? ''
|
||||||
|
if (message.includes('子分类')) {
|
||||||
|
throw new Error('请先删除该分类下的子分类')
|
||||||
|
}
|
||||||
|
if (message.includes('商品')) {
|
||||||
|
throw new Error('该分类下仍有商品,无法删除')
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
68
services/admin/productService.uts
Normal file
68
services/admin/productService.uts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { rpcOrNull, rpcOrValue } from '@/services/analytics/rpc.uts'
|
||||||
|
|
||||||
|
export type AdminProduct = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
image: string
|
||||||
|
price: number
|
||||||
|
stock: number
|
||||||
|
sales: number
|
||||||
|
status: number
|
||||||
|
created_at: string
|
||||||
|
category_name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProductPageResult = {
|
||||||
|
total: number
|
||||||
|
items: Array<AdminProduct>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取商品列表
|
||||||
|
*/
|
||||||
|
export async function fetchAdminProductPage(
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
filters: {
|
||||||
|
name?: string,
|
||||||
|
status?: number,
|
||||||
|
categoryId?: string
|
||||||
|
}
|
||||||
|
): Promise<ProductPageResult> {
|
||||||
|
const res = await rpcOrNull('rpc_admin_product_list', {
|
||||||
|
p_page: page,
|
||||||
|
p_page_size: pageSize,
|
||||||
|
p_name: filters.name ?? null,
|
||||||
|
p_status: filters.status ?? null,
|
||||||
|
p_category_id: filters.categoryId ?? null
|
||||||
|
} as UTSJSONObject)
|
||||||
|
|
||||||
|
if (res == null) {
|
||||||
|
return { total: 0, items: [] as Array<AdminProduct> }
|
||||||
|
}
|
||||||
|
|
||||||
|
const anyTotal = (res as any).total
|
||||||
|
const anyItems = (res as any).items
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: typeof anyTotal === 'number' ? anyTotal : 0,
|
||||||
|
items: Array.isArray(anyItems) ? anyItems : [] as Array<AdminProduct>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新商品状态 (上架/下架/回收站)
|
||||||
|
* @param status 1:上架 2:下架 3:草稿 4:删除
|
||||||
|
*/
|
||||||
|
export async function updateAdminProductStatus(productId: string, status: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const ok = await rpcOrValue('rpc_admin_product_update_status', {
|
||||||
|
p_product_id: productId,
|
||||||
|
p_status: status
|
||||||
|
} as UTSJSONObject)
|
||||||
|
return ok === true
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('更新商品状态失败:', e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { rpcOrNull, rpcOrEmptyArray } from '@/services/analytics/rpc.uts'
|
import { rpcOrNull, rpcOrEmptyArray, rpcOrValue } from '@/services/analytics/rpc.uts'
|
||||||
|
|
||||||
export async function fetchRefundOrderPage(
|
export async function fetchRefundOrderPage(
|
||||||
page: number,
|
page: number,
|
||||||
@@ -24,6 +24,28 @@ export async function fetchRefundOrderPage(
|
|||||||
return { total, items }
|
return { total, items }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchWriteOffRecordPage(
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
search: string | null = null
|
||||||
|
): Promise<{ total: number; items: Array<any> }> {
|
||||||
|
const res = await rpcOrNull('rpc_admin_write_off_record_list', {
|
||||||
|
p_page: page,
|
||||||
|
p_page_size: pageSize,
|
||||||
|
p_search: search
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
if (res == null) return { total: 0, items: [] as Array<any> }
|
||||||
|
|
||||||
|
const anyTotal = (res as any).total
|
||||||
|
const anyItems = (res as any).items
|
||||||
|
|
||||||
|
const total = typeof anyTotal === 'number' ? anyTotal : parseInt(String(anyTotal ?? '0'))
|
||||||
|
const items = Array.isArray(anyItems) ? (anyItems as Array<any>) : ([] as Array<any>)
|
||||||
|
|
||||||
|
return { total, items }
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchCashierOrderPage(
|
export async function fetchCashierOrderPage(
|
||||||
page: number,
|
page: number,
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
@@ -48,31 +70,24 @@ export async function fetchCashierOrderPage(
|
|||||||
return { total, items }
|
return { total, items }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchWriteOffRecordPage(
|
|
||||||
page: number,
|
|
||||||
pageSize: number,
|
|
||||||
search: string | null = null
|
|
||||||
): Promise<{ total: number; items: Array<any> }> {
|
|
||||||
const res = await rpcOrNull('rpc_admin_write_off_record_list', {
|
|
||||||
p_page: page,
|
|
||||||
p_page_size: pageSize,
|
|
||||||
p_search: search
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
if (res == null) return { total: 0, items: [] as Array<any> }
|
|
||||||
|
|
||||||
const anyTotal = (res as any).total
|
|
||||||
const anyItems = (res as any).items
|
|
||||||
|
|
||||||
const total = typeof anyTotal === 'number' ? anyTotal : parseInt(String(anyTotal ?? '0'))
|
|
||||||
const items = Array.isArray(anyItems) ? (anyItems as Array<any>) : ([] as Array<any>)
|
|
||||||
|
|
||||||
return { total, items }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchOrderSourceStats(startTime: string, endTime: string): Promise<Array<any>> {
|
export async function fetchOrderSourceStats(startTime: string, endTime: string): Promise<Array<any>> {
|
||||||
return (await rpcOrEmptyArray('rpc_admin_order_source_stats', {
|
return (await rpcOrEmptyArray('rpc_admin_order_source_stats', {
|
||||||
p_start_time: startTime,
|
p_start_time: startTime,
|
||||||
p_end_time: endTime
|
p_end_time: endTime
|
||||||
} as any)) as any
|
} as any)) as any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getOrderSettings(): Promise<UTSJSONObject | null> {
|
||||||
|
const res = await rpcOrValue('rpc_admin_system_config_get', {
|
||||||
|
p_key: 'order_settings'
|
||||||
|
} as any)
|
||||||
|
return res as any
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveOrderSettings(config: UTSJSONObject): Promise<boolean> {
|
||||||
|
const res = await rpcOrValue('rpc_admin_system_config_save', {
|
||||||
|
p_key: 'order_settings',
|
||||||
|
p_value: config
|
||||||
|
} as any)
|
||||||
|
return res === true
|
||||||
|
}
|
||||||
|
|||||||
49
types/admin/finance.uts
Normal file
49
types/admin/finance.uts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export type FinanceOverview = {
|
||||||
|
recharge_amount: number
|
||||||
|
recharge_count: number
|
||||||
|
extract_amount: number
|
||||||
|
extract_count: number
|
||||||
|
total_user_balance: number
|
||||||
|
total_user_brokerage: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtractRecord = {
|
||||||
|
id: string
|
||||||
|
uid: string
|
||||||
|
real_name: string | null
|
||||||
|
extract_type: string
|
||||||
|
bank_code: string | null
|
||||||
|
bank_address: string | null
|
||||||
|
alipay_code: string | null
|
||||||
|
wechat_code: string | null
|
||||||
|
extract_price: number
|
||||||
|
service_fee: number
|
||||||
|
balance: number
|
||||||
|
status: number
|
||||||
|
refusal_reason: string | null
|
||||||
|
admin_id: string | null
|
||||||
|
payment_time: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserBillRecord = {
|
||||||
|
id: string
|
||||||
|
uid: string
|
||||||
|
link_id: string | null
|
||||||
|
pm: number
|
||||||
|
title: string
|
||||||
|
category: string
|
||||||
|
type: string
|
||||||
|
number: number
|
||||||
|
balance: number
|
||||||
|
mark: string | null
|
||||||
|
status: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PagedResult<T> = {
|
||||||
|
total: number
|
||||||
|
items: Array<T>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user