diff --git a/docs/sql/10_schema/distribution/ak_distribution_agents_v1.sql b/docs/sql/10_schema/distribution/ak_distribution_agents_v1.sql
new file mode 100644
index 00000000..4757ce83
--- /dev/null
+++ b/docs/sql/10_schema/distribution/ak_distribution_agents_v1.sql
@@ -0,0 +1,41 @@
+-- =====================================================================================
+-- Schema: 分销代理商管理表
+-- 位置:docs/sql/10_schema/distribution/ak_distribution_agents_v1.sql
+-- 说明:管理事业部旗下的代理商,按商家隔离。
+-- =====================================================================================
+
+CREATE TABLE IF NOT EXISTS public.ak_distribution_agents (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+ division_id UUID NOT NULL REFERENCES public.ak_distribution_divisions(id) ON DELETE CASCADE,
+ uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ name TEXT NOT NULL, -- 代理商名称(或备注名)
+ status BOOLEAN DEFAULT true, -- 状态: true开启, false关闭
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now(),
+
+ -- 约束:一个用户在一个商家下只能成为一个代理商
+ UNIQUE(merchant_id, uid)
+);
+
+-- 启用 RLS
+ALTER TABLE public.ak_distribution_agents ENABLE ROW LEVEL SECURITY;
+
+-- 权限策略:商家仅能管理自己的代理商
+CREATE POLICY "Merchants manage their own agents"
+ON public.ak_distribution_agents FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许查看
+CREATE POLICY "Authenticated users view active agents"
+ON public.ak_distribution_agents FOR SELECT
+TO authenticated
+USING (status = true);
+
+-- 索引
+CREATE INDEX IF NOT EXISTS idx_agents_merchant ON public.ak_distribution_agents(merchant_id);
+CREATE INDEX IF NOT EXISTS idx_agents_division ON public.ak_distribution_agents(division_id);
diff --git a/docs/sql/10_schema/distribution/ak_distribution_division_applications_v1.sql b/docs/sql/10_schema/distribution/ak_distribution_division_applications_v1.sql
new file mode 100644
index 00000000..2aae4fcd
--- /dev/null
+++ b/docs/sql/10_schema/distribution/ak_distribution_division_applications_v1.sql
@@ -0,0 +1,45 @@
+-- =====================================================================================
+-- Schema: 分销事业部申请表
+-- 位置:docs/sql/10_schema/distribution/ak_distribution_division_applications_v1.sql
+-- 说明:记录用户申请加入事业部成为代理商的流水,支持审核流转,按商家隔离。
+-- =====================================================================================
+
+CREATE TABLE IF NOT EXISTS public.ak_distribution_division_applications (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+ uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ division_id UUID NOT NULL REFERENCES public.ak_distribution_divisions(id) ON DELETE CASCADE,
+
+ name TEXT NOT NULL, -- 申请人填写的代理商名称
+ phone TEXT NOT NULL, -- 申请人联系电话
+ images JSONB DEFAULT '[]'::jsonb, -- 申请附件图片 (数组)
+
+ status INTEGER DEFAULT 1, -- 状态: 1待审核, 2已同意, 3已拒绝
+ admin_remark TEXT, -- 审核备注
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 启用 RLS
+ALTER TABLE public.ak_distribution_division_applications ENABLE ROW LEVEL SECURITY;
+
+-- 权限策略
+CREATE POLICY "Merchants manage their own applications"
+ON public.ak_distribution_division_applications FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许用户提交和查看自己的申请
+CREATE POLICY "Users handle own applications"
+ON public.ak_distribution_division_applications FOR ALL
+TO authenticated
+USING (uid = auth.uid())
+WITH CHECK (uid = auth.uid());
+
+-- 索引
+CREATE INDEX IF NOT EXISTS idx_div_app_merchant ON public.ak_distribution_division_applications(merchant_id);
+CREATE INDEX IF NOT EXISTS idx_div_app_uid ON public.ak_distribution_division_applications(uid);
+CREATE INDEX IF NOT EXISTS idx_div_app_status ON public.ak_distribution_division_applications(status);
diff --git a/docs/sql/10_schema/distribution/ak_distribution_divisions_v1.sql b/docs/sql/10_schema/distribution/ak_distribution_divisions_v1.sql
new file mode 100644
index 00000000..e0e58126
--- /dev/null
+++ b/docs/sql/10_schema/distribution/ak_distribution_divisions_v1.sql
@@ -0,0 +1,46 @@
+-- =====================================================================================
+-- Schema: 分销事业部管理表
+-- 位置:docs/sql/10_schema/distribution/ak_distribution_divisions_v1.sql
+-- 说明:管理分销体系中的事业部,支持独立分销比例、邀请码及有效期,按商家隔离。
+-- =====================================================================================
+
+CREATE TABLE IF NOT EXISTS public.ak_distribution_divisions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ uid UUID NOT NULL REFERENCES public.ak_users(id), -- 事业部负责人UID
+ name TEXT NOT NULL, -- 事业部名称
+ invite_code TEXT UNIQUE NOT NULL, -- 事业部专属邀请码
+
+ ratio DECIMAL(5,2) DEFAULT 0, -- 事业部额外分销比例 (%)
+ agent_count INTEGER DEFAULT 0, -- 下属代理商数量 (由程序或触发器维护)
+
+ end_time TIMESTAMPTZ, -- 协议截止时间
+ status BOOLEAN DEFAULT true, -- 状态: true开启, false关闭
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now(),
+
+ -- 约束:一个用户在一个商家下只能负责一个事业部
+ UNIQUE(merchant_id, uid)
+);
+
+-- 启用 RLS
+ALTER TABLE public.ak_distribution_divisions ENABLE ROW LEVEL SECURITY;
+
+-- 权限策略
+CREATE POLICY "Merchants manage their own divisions"
+ON public.ak_distribution_divisions FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许查看
+CREATE POLICY "Authenticated users view active divisions"
+ON public.ak_distribution_divisions FOR SELECT
+TO authenticated
+USING (status = true);
+
+-- 索引
+CREATE INDEX IF NOT EXISTS idx_divisions_merchant ON public.ak_distribution_divisions(merchant_id);
+CREATE INDEX IF NOT EXISTS idx_divisions_uid ON public.ak_distribution_divisions(uid);
diff --git a/docs/sql/10_schema/marketing/ak_advanced_marketing_v1.sql b/docs/sql/10_schema/marketing/ak_advanced_marketing_v1.sql
new file mode 100644
index 00000000..74830c80
--- /dev/null
+++ b/docs/sql/10_schema/marketing/ak_advanced_marketing_v1.sql
@@ -0,0 +1,77 @@
+-- =====================================================================================
+-- Schema: 秒杀与拼团活动表
+-- 位置:docs/sql/10_schema/marketing/ak_advanced_marketing_v1.sql
+-- 说明:管理秒杀活动与拼团活动,按商家隔离。
+-- =====================================================================================
+
+-- 1. 秒杀活动表
+CREATE TABLE IF NOT EXISTS public.ak_seckill_activities (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ title TEXT NOT NULL, -- 活动标题
+ single_limit INTEGER DEFAULT 1, -- 单次限购
+ total_limit INTEGER DEFAULT 10, -- 总购买数量限制
+ product_count INTEGER DEFAULT 0, -- 包含商品数量
+ time_range TEXT NOT NULL, -- 活动时段 (如 "06:00-24:00")
+
+ start_date TIMESTAMPTZ NOT NULL, -- 开始日期
+ end_date TIMESTAMPTZ NOT NULL, -- 结束日期
+
+ status BOOLEAN DEFAULT true, -- 状态: true开启, false关闭
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 2. 拼团活动表 (开团记录)
+CREATE TABLE IF NOT EXISTS public.ak_combination_activities (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ uid UUID NOT NULL REFERENCES public.ak_users(id), -- 开团团长
+ product_id UUID NOT NULL REFERENCES public.ml_products(id), -- 拼团商品
+
+ people INTEGER DEFAULT 2, -- 几人团
+ count_people INTEGER DEFAULT 1, -- 当前几人参加
+
+ start_time TIMESTAMPTZ DEFAULT now(), -- 开团时间
+ stop_time TIMESTAMPTZ NOT NULL, -- 结束时间
+
+ status TEXT NOT NULL DEFAULT 'ongoing', -- ongoing进行中, pending未完成, ended已成功
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now(),
+
+ CONSTRAINT chk_comb_status CHECK (status IN ('ongoing', 'pending', 'ended'))
+);
+
+-- 3. 启用 RLS
+ALTER TABLE public.ak_seckill_activities ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ak_combination_activities ENABLE ROW LEVEL SECURITY;
+
+-- 4. 创建权限策略 (按 merchant_id 隔离)
+-- 秒杀策略
+CREATE POLICY "Merchants can manage their own seckill activities"
+ON public.ak_seckill_activities FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 拼团策略
+CREATE POLICY "Merchants can manage their own combination activities"
+ON public.ak_combination_activities FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许所有认证用户查看(用于移动端展示)
+CREATE POLICY "Anyone can view active marketing activities"
+ON public.ak_seckill_activities FOR SELECT
+TO authenticated
+USING (status = true);
+
+CREATE POLICY "Anyone can view ongoing combinations"
+ON public.ak_combination_activities FOR SELECT
+TO authenticated
+USING (true);
diff --git a/docs/sql/10_schema/marketing/ak_bargain_groupbuy_v1.sql b/docs/sql/10_schema/marketing/ak_bargain_groupbuy_v1.sql
new file mode 100644
index 00000000..a963e5fb
--- /dev/null
+++ b/docs/sql/10_schema/marketing/ak_bargain_groupbuy_v1.sql
@@ -0,0 +1,78 @@
+-- =====================================================================================
+-- Schema: 砍价与团购活动表
+-- 位置:docs/sql/10_schema/marketing/ak_bargain_groupbuy_v1.sql
+-- 说明:管理砍价与团购活动,按商家隔离。
+-- =====================================================================================
+
+-- 1. 砍价活动表
+CREATE TABLE IF NOT EXISTS public.ak_marketing_bargains (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+ product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
+
+ title TEXT NOT NULL, -- 活动标题
+ min_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 砍价最低价
+ stock INTEGER DEFAULT 0, -- 活动库存
+
+ start_time TIMESTAMPTZ NOT NULL, -- 开始时间
+ stop_time TIMESTAMPTZ NOT NULL, -- 结束时间
+
+ status BOOLEAN DEFAULT true, -- 状态: true开启, false关闭
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 2. 团购活动表
+CREATE TABLE IF NOT EXISTS public.ak_marketing_groupbuys (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+ product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
+
+ title TEXT NOT NULL, -- 活动标题
+ price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 团购价格
+ people INTEGER DEFAULT 2, -- 成团人数要求
+ stock INTEGER DEFAULT 0, -- 活动库存
+
+ start_time TIMESTAMPTZ NOT NULL, -- 开始时间
+ stop_time TIMESTAMPTZ NOT NULL, -- 结束时间
+
+ status BOOLEAN DEFAULT true, -- 状态: true开启, false关闭
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 3. 启用 RLS
+ALTER TABLE public.ak_marketing_bargains ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ak_marketing_groupbuys ENABLE ROW LEVEL SECURITY;
+
+-- 4. 创建权限策略 (按 merchant_id 隔离)
+-- 砍价策略
+CREATE POLICY "Merchants can manage their own bargains"
+ON public.ak_marketing_bargains FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 团购策略
+CREATE POLICY "Merchants can manage their own groupbuys"
+ON public.ak_marketing_groupbuys FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许所有认证用户查看(用于移动端展示)
+CREATE POLICY "Anyone can view active marketing activities"
+ON public.ak_marketing_bargains FOR SELECT
+TO authenticated
+USING (status = true);
+
+CREATE POLICY "Anyone can view active groupbuys"
+ON public.ak_marketing_groupbuys FOR SELECT
+TO authenticated
+USING (status = true);
+
+-- 5. 索引
+CREATE INDEX IF NOT EXISTS idx_bargains_merchant ON public.ak_marketing_bargains(merchant_id);
+CREATE INDEX IF NOT EXISTS idx_groupbuys_merchant ON public.ak_marketing_groupbuys(merchant_id);
diff --git a/docs/sql/10_schema/marketing/ak_live_products_v1.sql b/docs/sql/10_schema/marketing/ak_live_products_v1.sql
new file mode 100644
index 00000000..a441650e
--- /dev/null
+++ b/docs/sql/10_schema/marketing/ak_live_products_v1.sql
@@ -0,0 +1,41 @@
+-- =====================================================================================
+-- Schema: 直播商品管理表
+-- 位置:docs/sql/10_schema/marketing/ak_live_products_v1.sql
+-- 说明:管理直播活动关联的商品,支持直播价设置与审核状态,按商家隔离。
+-- =====================================================================================
+
+CREATE TABLE IF NOT EXISTS public.ak_marketing_live_products (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+ product_id UUID NOT NULL REFERENCES public.ml_products(id) ON DELETE CASCADE,
+
+ live_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 直播专属价
+ stock INTEGER DEFAULT 0, -- 直播可用库存
+
+ audit_status INTEGER DEFAULT 1, -- 审核状态: 1待审核, 2审核通过, 3审核驳回
+ is_show BOOLEAN DEFAULT true, -- 是否在直播间显示
+
+ sort_order INTEGER DEFAULT 0,
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 启用 RLS
+ALTER TABLE public.ak_marketing_live_products ENABLE ROW LEVEL SECURITY;
+
+-- 权限策略:商家仅能管理自己的直播商品
+CREATE POLICY "Merchants can manage their own live products"
+ON public.ak_marketing_live_products FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许查看审核通过的商品
+CREATE POLICY "Anyone can view approved live products"
+ON public.ak_marketing_live_products FOR SELECT
+TO authenticated
+USING (audit_status = 2 AND is_show = true);
+
+-- 索引
+CREATE INDEX IF NOT EXISTS idx_live_products_merchant ON public.ak_marketing_live_products(merchant_id);
+CREATE INDEX IF NOT EXISTS idx_live_products_product ON public.ak_marketing_live_products(product_id);
diff --git a/docs/sql/10_schema/marketing/ak_lottery_live_v1.sql b/docs/sql/10_schema/marketing/ak_lottery_live_v1.sql
new file mode 100644
index 00000000..28d8f959
--- /dev/null
+++ b/docs/sql/10_schema/marketing/ak_lottery_live_v1.sql
@@ -0,0 +1,93 @@
+-- =====================================================================================
+-- Schema: 抽奖与直播管理表
+-- 位置:docs/sql/10_schema/marketing/ak_lottery_live_v1.sql
+-- 说明:管理抽奖活动、奖品、主播及直播间,按商家隔离。
+-- =====================================================================================
+
+-- 1. 抽奖活动表
+CREATE TABLE IF NOT EXISTS public.ak_marketing_lotteries (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ name TEXT NOT NULL, -- 活动名称
+ type INTEGER DEFAULT 1, -- 活动类型: 1积分抽奖, 2订单评价, 3订单支付
+
+ start_time TIMESTAMPTZ NOT NULL, -- 开始时间
+ end_time TIMESTAMPTZ NOT NULL, -- 结束时间
+
+ is_open BOOLEAN DEFAULT true, -- 是否开启
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 2. 抽奖奖品表
+CREATE TABLE IF NOT EXISTS public.ak_marketing_lottery_prizes (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ lottery_id UUID NOT NULL REFERENCES public.ak_marketing_lotteries(id) ON DELETE CASCADE,
+
+ name TEXT NOT NULL, -- 奖品名称
+ prize_type TEXT NOT NULL, -- 奖品类型: points, balance, coupon, physical
+ amount DECIMAL(12,2) DEFAULT 0, -- 奖励面值/数量
+ stock INTEGER DEFAULT 0, -- 奖品库存
+ probability DECIMAL(5,2) DEFAULT 0, -- 中奖概率 (0-100)
+
+ sort_order INTEGER DEFAULT 0,
+ created_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 3. 直播主播表
+CREATE TABLE IF NOT EXISTS public.ak_marketing_live_anchors (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ nickname TEXT NOT NULL, -- 主播昵称
+ wechat TEXT, -- 微信号
+ phone TEXT, -- 联系电话
+ avatar_url TEXT, -- 头像
+
+ status BOOLEAN DEFAULT true, -- 状态
+ created_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 4. 直播间管理表
+CREATE TABLE IF NOT EXISTS public.ak_marketing_live_rooms (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+ anchor_id UUID REFERENCES public.ak_marketing_live_anchors(id) ON DELETE SET NULL,
+
+ name TEXT NOT NULL, -- 直播间名称
+ background_url TEXT, -- 背景图
+ share_img_url TEXT, -- 分享图
+
+ start_time TIMESTAMPTZ NOT NULL, -- 开始时间
+ end_time TIMESTAMPTZ NOT NULL, -- 计划结束时间
+
+ sort INTEGER DEFAULT 0, -- 排序
+ type TEXT DEFAULT 'phone', -- 类型: phone手机直播等
+
+ like_enabled BOOLEAN DEFAULT true, -- 开启点赞
+ sale_enabled BOOLEAN DEFAULT true, -- 开启卖货
+ comment_enabled BOOLEAN DEFAULT true, -- 开启评论
+
+ is_show BOOLEAN DEFAULT true, -- 是否显示
+ live_status INTEGER DEFAULT 1, -- 1未开始, 2直播中, 3暂停, 4已结束
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 5. 启用 RLS
+ALTER TABLE public.ak_marketing_lotteries ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ak_marketing_lottery_prizes ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ak_marketing_live_anchors ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ak_marketing_live_rooms ENABLE ROW LEVEL SECURITY;
+
+-- 6. 创建权限策略 (按 merchant_id 隔离)
+CREATE POLICY "Merchants manage their own lotteries" ON public.ak_marketing_lotteries FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
+CREATE POLICY "Merchants manage their own anchors" ON public.ak_marketing_live_anchors FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
+CREATE POLICY "Merchants manage their own rooms" ON public.ak_marketing_live_rooms FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
+
+-- 允许查看
+CREATE POLICY "Users can view lotteries" ON public.ak_marketing_lotteries FOR SELECT TO authenticated USING (is_open = true);
+CREATE POLICY "Users can view active rooms" ON public.ak_marketing_live_rooms FOR SELECT TO authenticated USING (is_show = true);
diff --git a/docs/sql/10_schema/marketing/ak_marketing_checkin_configs_v1.sql b/docs/sql/10_schema/marketing/ak_marketing_checkin_configs_v1.sql
new file mode 100644
index 00000000..af420d45
--- /dev/null
+++ b/docs/sql/10_schema/marketing/ak_marketing_checkin_configs_v1.sql
@@ -0,0 +1,39 @@
+-- =====================================================================================
+-- Schema: 打卡/签到增强配置表
+-- 位置:docs/sql/10_schema/marketing/ak_marketing_checkin_configs_v1.sql
+-- 说明:管理打卡开关、模式、提醒及基础奖励(积分/经验),按商家隔离。
+-- =====================================================================================
+
+CREATE TABLE IF NOT EXISTS public.ak_marketing_checkin_configs (
+ id TEXT PRIMARY KEY DEFAULT 'checkin_config',
+ merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ is_open BOOLEAN DEFAULT true, -- 签到开关
+ mode TEXT DEFAULT 'none', -- 签到模式: none(无限制), week(周循环), month(月循环)
+ notice_enabled BOOLEAN DEFAULT false, -- 签到提醒开关
+
+ integral_reward INTEGER DEFAULT 10, -- 每日签到赠送积分
+ exp_reward INTEGER DEFAULT 1, -- 每日签到赠送经验
+
+ updated_at TIMESTAMPTZ DEFAULT now(),
+ updated_by UUID REFERENCES auth.users(id)
+);
+
+-- 启用 RLS
+ALTER TABLE public.ak_marketing_checkin_configs ENABLE ROW LEVEL SECURITY;
+
+-- 权限策略:商家仅能管理自己的打卡配置
+CREATE POLICY "Merchants manage their own checkin configs"
+ON public.ak_marketing_checkin_configs FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许所有认证用户查看(用于前台展示)
+CREATE POLICY "Anyone can view checkin config"
+ON public.ak_marketing_checkin_configs FOR SELECT
+TO authenticated
+USING (true);
+
+-- 插入初始化数据(为每个管理员/商家初始化一条)
+-- 实际应在商家创建时触发,此处先预留
diff --git a/docs/sql/10_schema/marketing/ak_marketing_newcomer_config_v1.sql b/docs/sql/10_schema/marketing/ak_marketing_newcomer_config_v1.sql
new file mode 100644
index 00000000..0d1c3969
--- /dev/null
+++ b/docs/sql/10_schema/marketing/ak_marketing_newcomer_config_v1.sql
@@ -0,0 +1,35 @@
+-- =====================================================================================
+-- Schema: 新人礼配置表
+-- 位置:docs/sql/10_schema/marketing/ak_marketing_newcomer_config_v1.sql
+-- 说明:管理新用户注册后的奖励(余额、积分、优惠券),按商家隔离。
+-- =====================================================================================
+
+CREATE TABLE IF NOT EXISTS public.ak_marketing_newcomer_config (
+ id TEXT PRIMARY KEY DEFAULT 'newcomer_config',
+ merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ balance_reward DECIMAL(12,2) DEFAULT 0.00, -- 赠送余额
+ integral_reward INTEGER DEFAULT 0, -- 赠送积分
+
+ -- 赠送优惠券 (JSONB 格式): [{ "id": "coupon_uuid", "name": "显示名称", "desc": "发放描述" }]
+ coupons_json JSONB DEFAULT '[]'::jsonb,
+
+ updated_at TIMESTAMPTZ DEFAULT now(),
+ updated_by UUID REFERENCES auth.users(id)
+);
+
+-- 启用 RLS
+ALTER TABLE public.ak_marketing_newcomer_config ENABLE ROW LEVEL SECURITY;
+
+-- 权限策略:商家仅能管理自己的新人礼配置
+CREATE POLICY "Merchants manage their own newcomer configs"
+ON public.ak_marketing_newcomer_config FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许查看配置(用于移动端展示)
+CREATE POLICY "Anyone can view newcomer config"
+ON public.ak_marketing_newcomer_config FOR SELECT
+TO authenticated
+USING (true);
diff --git a/docs/sql/10_schema/marketing/ak_marketing_signin_logs_v1.sql b/docs/sql/10_schema/marketing/ak_marketing_signin_logs_v1.sql
new file mode 100644
index 00000000..425170e3
--- /dev/null
+++ b/docs/sql/10_schema/marketing/ak_marketing_signin_logs_v1.sql
@@ -0,0 +1,36 @@
+-- =====================================================================================
+-- Schema: 签到记录表
+-- 位置:docs/sql/10_schema/marketing/ak_marketing_signin_logs_v1.sql
+-- 说明:记录用户每日签到的详细流水,按商家隔离。
+-- =====================================================================================
+
+CREATE TABLE IF NOT EXISTS public.ak_marketing_signin_logs (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+ uid UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ points INTEGER NOT NULL DEFAULT 0, -- 本次签到获得的积分
+ is_continuous_reward BOOLEAN DEFAULT false, -- 是否包含连续签到额外奖励
+
+ created_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 启用 RLS
+ALTER TABLE public.ak_marketing_signin_logs ENABLE ROW LEVEL SECURITY;
+
+-- 权限策略:商家仅能管理/查看自己的签到记录
+CREATE POLICY "Merchants manage their own signin logs"
+ON public.ak_marketing_signin_logs FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许用户查看自己的签到记录
+CREATE POLICY "Users view own signin logs"
+ON public.ak_marketing_signin_logs FOR SELECT
+TO authenticated
+USING (uid = auth.uid());
+
+-- 索引
+CREATE INDEX IF NOT EXISTS idx_signin_logs_merchant ON public.ak_marketing_signin_logs(merchant_id);
+CREATE INDEX IF NOT EXISTS idx_signin_logs_uid ON public.ak_marketing_signin_logs(uid, created_at DESC);
diff --git a/docs/sql/10_schema/marketing/ak_member_management_v1.sql b/docs/sql/10_schema/marketing/ak_member_management_v1.sql
new file mode 100644
index 00000000..ede75643
--- /dev/null
+++ b/docs/sql/10_schema/marketing/ak_member_management_v1.sql
@@ -0,0 +1,71 @@
+-- =====================================================================================
+-- Schema: 付费会员管理相关表
+-- 位置:docs/sql/10_schema/marketing/ak_member_management_v1.sql
+-- 说明:管理会员卡类型、权益内容及基础配置,按商家隔离。
+-- =====================================================================================
+
+-- 1. 会员卡类型表
+CREATE TABLE IF NOT EXISTS public.ak_marketing_member_types (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ name TEXT NOT NULL, -- 会员名 (如: 月卡, 年卡)
+ duration_days INTEGER DEFAULT 30, -- 有效期(天),0表示永久
+ price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 原价
+ discount_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 优惠价/实际支付价
+
+ is_open BOOLEAN DEFAULT true, -- 是否开启
+ sort_order INTEGER DEFAULT 0, -- 排序
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 2. 会员权益表
+CREATE TABLE IF NOT EXISTS public.ak_marketing_member_rights (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ name TEXT NOT NULL, -- 权益名称
+ description TEXT, -- 权益简介
+ icon_url TEXT, -- 权益图标
+
+ is_show BOOLEAN DEFAULT true, -- 是否展示
+ sort_order INTEGER DEFAULT 0, -- 排序
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 3. 会员基础配置表 (每个商家一条记录)
+CREATE TABLE IF NOT EXISTS public.ak_marketing_member_config (
+ id TEXT PRIMARY KEY DEFAULT 'member_config',
+ merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ is_enabled BOOLEAN DEFAULT true, -- 是否开启付费会员功能
+ bg_img_url TEXT, -- 会员期内背景图
+ expire_bg_img_url TEXT, -- 会员到期背景图
+ rules_description TEXT, -- 会员规则说明文本
+
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 4. 启用 RLS
+ALTER TABLE public.ak_marketing_member_types ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ak_marketing_member_rights ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ak_marketing_member_config ENABLE ROW LEVEL SECURITY;
+
+-- 5. 创建权限策略 (按 merchant_id 隔离)
+-- 商家管理自己的数据
+CREATE POLICY "Merchants manage their own member types" ON public.ak_marketing_member_types FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
+CREATE POLICY "Merchants manage their own member rights" ON public.ak_marketing_member_rights FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
+CREATE POLICY "Merchants manage their own member config" ON public.ak_marketing_member_config FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
+
+-- 允许所有认证用户查看 (移动端展示)
+CREATE POLICY "Users can view active member types" ON public.ak_marketing_member_types FOR SELECT TO authenticated USING (is_open = true);
+CREATE POLICY "Users can view active member rights" ON public.ak_marketing_member_rights FOR SELECT TO authenticated USING (is_show = true);
+CREATE POLICY "Users can view member config" ON public.ak_marketing_member_config FOR SELECT TO authenticated USING (true);
+
+-- 6. 索引
+CREATE INDEX IF NOT EXISTS idx_member_types_merchant ON public.ak_marketing_member_types(merchant_id);
+CREATE INDEX IF NOT EXISTS idx_member_rights_merchant ON public.ak_marketing_member_rights(merchant_id);
diff --git a/docs/sql/10_schema/marketing/ak_recharge_management_v1.sql b/docs/sql/10_schema/marketing/ak_recharge_management_v1.sql
new file mode 100644
index 00000000..281d3d2d
--- /dev/null
+++ b/docs/sql/10_schema/marketing/ak_recharge_management_v1.sql
@@ -0,0 +1,48 @@
+-- =====================================================================================
+-- Schema: 充值配置与额度模板表
+-- 位置:docs/sql/10_schema/marketing/ak_recharge_management_v1.sql
+-- 说明:管理用户充值开关、最低金额及预设额度,按商家隔离。
+-- =====================================================================================
+
+-- 1. 充值基础配置表 (每个商家一条记录)
+CREATE TABLE IF NOT EXISTS public.ak_recharge_configs (
+ id TEXT PRIMARY KEY DEFAULT 'recharge_config',
+ merchant_id UUID NOT NULL UNIQUE REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ balance_enabled BOOLEAN DEFAULT true, -- 余额功能是否启用
+ recharge_notice TEXT, -- 充值注意事项说明
+ mp_recharge_enabled BOOLEAN DEFAULT false, -- 小程序充值开关
+ min_recharge_amount DECIMAL(12,2) DEFAULT 0.01, -- 最低充值金额
+
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 2. 充值额度模板表
+CREATE TABLE IF NOT EXISTS public.ak_recharge_quotas (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 售价(实际充值金额)
+ bonus_price DECIMAL(12,2) NOT NULL DEFAULT 0, -- 赠送金额
+
+ is_open BOOLEAN DEFAULT true, -- 是否可用
+ sort_order INTEGER DEFAULT 0, -- 排序
+
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 3. 启用 RLS
+ALTER TABLE public.ak_recharge_configs ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.ak_recharge_quotas ENABLE ROW LEVEL SECURITY;
+
+-- 4. 创建权限策略 (按 merchant_id 隔离)
+CREATE POLICY "Merchants manage their own recharge configs" ON public.ak_recharge_configs FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
+CREATE POLICY "Merchants manage their own recharge quotas" ON public.ak_recharge_quotas FOR ALL TO authenticated USING (merchant_id = auth.uid()) WITH CHECK (merchant_id = auth.uid());
+
+-- 允许所有认证用户查看
+CREATE POLICY "Users can view recharge configs" ON public.ak_recharge_configs FOR SELECT TO authenticated USING (true);
+CREATE POLICY "Users can view active recharge quotas" ON public.ak_recharge_quotas FOR SELECT TO authenticated USING (is_open = true);
+
+-- 5. 索引
+CREATE INDEX IF NOT EXISTS idx_recharge_quotas_merchant ON public.ak_recharge_quotas(merchant_id);
diff --git a/docs/sql/10_schema/marketing/ak_signin_configs_v1.sql b/docs/sql/10_schema/marketing/ak_signin_configs_v1.sql
new file mode 100644
index 00000000..56974d75
--- /dev/null
+++ b/docs/sql/10_schema/marketing/ak_signin_configs_v1.sql
@@ -0,0 +1,42 @@
+-- =====================================================================================
+-- Schema: 签到规则配置表
+-- 位置:docs/sql/10_schema/marketing/ak_signin_configs_v1.sql
+-- 说明:记录每日签到积分、连续签到奖励及规则说明,按商家隔离。
+-- =====================================================================================
+
+CREATE TABLE IF NOT EXISTS public.ak_signin_configs (
+ id TEXT PRIMARY KEY DEFAULT 'signin_config', -- 每个商家一个配置记录
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ is_enabled BOOLEAN DEFAULT true, -- 签到功能是否启用
+ daily_points INTEGER DEFAULT 10, -- 每日签到固定奖励积分
+
+ -- 连续签到奖励 (JSONB 格式): [{ "day": 3, "points": 20 }, { "day": 7, "points": 50 }]
+ continuous_rewards JSONB DEFAULT '[]'::jsonb,
+
+ rules_description TEXT DEFAULT '1.每日签到可获得积分奖励;\n2.连续签到满足天数可获得额外阶梯奖励;\n3.签到中断将重新从第一天开始计算。',
+
+ updated_at TIMESTAMPTZ DEFAULT now(),
+ updated_by UUID REFERENCES auth.users(id),
+
+ -- 约束:同一个商家只有一个签到配置记录
+ UNIQUE(merchant_id)
+);
+
+-- 启用 RLS
+ALTER TABLE public.ak_signin_configs ENABLE ROW LEVEL SECURITY;
+
+-- 权限策略:商家仅能管理自己的签到配置
+CREATE POLICY "Merchants can manage their own signin configs"
+ON public.ak_signin_configs
+FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许所有认证用户查看配置(用于移动端签到展示)
+CREATE POLICY "Authenticated users can view signin configs"
+ON public.ak_signin_configs
+FOR SELECT
+TO authenticated
+USING (true);
diff --git a/docs/sql/10_schema/product/ak_shipping_templates_v1.sql b/docs/sql/10_schema/product/ak_shipping_templates_v1.sql
new file mode 100644
index 00000000..31f81101
--- /dev/null
+++ b/docs/sql/10_schema/product/ak_shipping_templates_v1.sql
@@ -0,0 +1,37 @@
+-- =====================================================================================
+-- Schema: 运费模板表
+-- 位置:docs/sql/10_schema/product/ak_shipping_templates_v1.sql
+-- 说明:管理商家的运费计算规则,按商家隔离。
+-- =====================================================================================
+
+CREATE TABLE IF NOT EXISTS public.ak_shipping_templates (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ merchant_id UUID NOT NULL REFERENCES public.ak_users(id) ON DELETE CASCADE,
+
+ name TEXT NOT NULL, -- 模板名称
+ calc_method TEXT DEFAULT 'piece', -- 计费方式: piece(件数), weight(重量), volume(体积)
+ is_free_shipping BOOLEAN DEFAULT false, -- 是否包邮
+
+ sort_order INTEGER DEFAULT 0,
+ created_at TIMESTAMPTZ DEFAULT now(),
+ updated_at TIMESTAMPTZ DEFAULT now()
+);
+
+-- 启用 RLS
+ALTER TABLE public.ak_shipping_templates ENABLE ROW LEVEL SECURITY;
+
+-- 权限策略:商家管理自己的模板
+CREATE POLICY "Merchants manage own shipping templates"
+ON public.ak_shipping_templates FOR ALL
+TO authenticated
+USING (merchant_id = auth.uid())
+WITH CHECK (merchant_id = auth.uid());
+
+-- 允许查看
+CREATE POLICY "Authenticated users view shipping templates"
+ON public.ak_shipping_templates FOR SELECT
+TO authenticated
+USING (true);
+
+-- 索引
+CREATE INDEX IF NOT EXISTS idx_shipping_templates_merchant ON public.ak_shipping_templates(merchant_id);
diff --git a/docs/sql/10_schema/product/ml_products_ext_v1.sql b/docs/sql/10_schema/product/ml_products_ext_v1.sql
new file mode 100644
index 00000000..cdca721d
--- /dev/null
+++ b/docs/sql/10_schema/product/ml_products_ext_v1.sql
@@ -0,0 +1,39 @@
+-- =====================================================================================
+-- Schema Update: ml_products 扩展字段 (物流、营销、高级设置)
+-- 位置:docs/sql/10_schema/product/ml_products_ext_v1.sql
+-- 说明:补齐商品编辑页 Step 3-6 所需的持久化字段。
+-- =====================================================================================
+
+DO $$
+BEGIN
+ -- 1. 物流设置:关联运费模板
+ IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_products' AND column_name = 'shipping_template_id') THEN
+ ALTER TABLE public.ml_products ADD COLUMN shipping_template_id UUID REFERENCES public.ak_shipping_templates(id) ON DELETE SET NULL;
+ COMMENT ON COLUMN public.ml_products.shipping_template_id IS '关联运费模板ID';
+ END IF;
+
+ -- 2. 营销设置:赠送积分
+ IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_products' AND column_name = 'give_integral') THEN
+ ALTER TABLE public.ml_products ADD COLUMN give_integral INTEGER DEFAULT 0;
+ COMMENT ON COLUMN public.ml_products.give_integral IS '购买赠送积分';
+ END IF;
+
+ -- 3. 高级设置:警戒库存
+ IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_products' AND column_name = 'stock_warning') THEN
+ ALTER TABLE public.ml_products ADD COLUMN stock_warning INTEGER DEFAULT 10;
+ COMMENT ON COLUMN public.ml_products.stock_warning IS '库存报警数值';
+ END IF;
+
+ -- 4. 高级设置:虚拟销量
+ IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_products' AND column_name = 'virtual_sales') THEN
+ ALTER TABLE public.ml_products ADD COLUMN virtual_sales INTEGER DEFAULT 0;
+ COMMENT ON COLUMN public.ml_products.virtual_sales IS '虚拟销量(展示用)';
+ END IF;
+
+ -- 5. 高级设置:排序
+ IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_products' AND column_name = 'sort_order') THEN
+ ALTER TABLE public.ml_products ADD COLUMN sort_order INTEGER DEFAULT 0;
+ COMMENT ON COLUMN public.ml_products.sort_order IS '商品排序权重';
+ END IF;
+
+END $$;
diff --git a/docs/sql/30_rpc/marketing/rpc_admin_get_integral_stats_v1.sql b/docs/sql/30_rpc/marketing/rpc_admin_get_integral_stats_v1.sql
new file mode 100644
index 00000000..1bc50a8e
--- /dev/null
+++ b/docs/sql/30_rpc/marketing/rpc_admin_get_integral_stats_v1.sql
@@ -0,0 +1,91 @@
+-- RPC: rpc_admin_get_integral_stats
+-- 位置:docs/sql/30_rpc/marketing/rpc_admin_get_integral_stats_v1.sql
+-- 说明:聚合统计积分概况(总额、趋势、分布)
+
+CREATE OR REPLACE FUNCTION public.rpc_admin_get_integral_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_total_stats RECORD;
+ v_trend_data JSONB;
+ v_source_dist JSONB;
+ v_consume_dist 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;
+
+ -- 2. 计算核心指标 (所有时间)
+ SELECT
+ COALESCE(SUM(CASE WHEN pm = 1 THEN number ELSE -number END), 0) as current_total,
+ COALESCE(SUM(CASE WHEN pm = 1 THEN number ELSE 0 END), 0) as cumulative_income,
+ COALESCE(SUM(CASE WHEN pm = 0 THEN number ELSE 0 END), 0) as cumulative_expend
+ INTO v_total_stats
+ FROM public.ml_user_bill
+ WHERE category = 'integral' AND status = 1;
+
+ -- 3. 趋势数据 (按日聚合)
+ SELECT jsonb_agg(t) INTO v_trend_data
+ FROM (
+ SELECT
+ to_char(date_trunc('day', gs.day), 'MM-DD') AS date_group,
+ COALESCE((SELECT SUM(number) FROM public.ml_user_bill b
+ WHERE b.category = 'integral' AND b.pm = 1 AND b.status = 1
+ AND date_trunc('day', b.created_at) = gs.day), 0) as income,
+ COALESCE((SELECT SUM(number) FROM public.ml_user_bill b
+ WHERE b.category = 'integral' AND b.pm = 0 AND b.status = 1
+ AND date_trunc('day', b.created_at) = gs.day), 0) as expend
+ FROM generate_series(date_trunc('day', p_start_time), date_trunc('day', p_end_time), '1 day'::interval) gs(day)
+ ORDER BY gs.day ASC
+ ) t;
+
+ -- 4. 来源分布 (按 type 分组)
+ SELECT jsonb_agg(t) INTO v_source_dist
+ FROM (
+ SELECT
+ type as label,
+ SUM(number) as value,
+ ROUND((SUM(number) * 100 / NULLIF(v_total_stats.cumulative_income, 0)), 2) as percent
+ FROM public.ml_user_bill
+ WHERE category = 'integral' AND pm = 1 AND status = 1
+ GROUP BY type
+ ORDER BY value DESC
+ ) t;
+
+ -- 5. 消耗分布 (按 type 分组)
+ SELECT jsonb_agg(t) INTO v_consume_dist
+ FROM (
+ SELECT
+ type as label,
+ SUM(number) as value,
+ ROUND((SUM(number) * 100 / NULLIF(v_total_stats.cumulative_expend, 0)), 2) as percent
+ FROM public.ml_user_bill
+ WHERE category = 'integral' AND pm = 0 AND status = 1
+ GROUP BY type
+ ORDER BY value DESC
+ ) t;
+
+ RETURN jsonb_build_object(
+ 'totals', jsonb_build_object(
+ 'current', v_total_stats.current_total,
+ 'income', v_total_stats.cumulative_income,
+ 'expend', v_total_stats.cumulative_expend
+ ),
+ 'trend', COALESCE(v_trend_data, '[]'::jsonb),
+ 'sources', COALESCE(v_source_dist, '[]'::jsonb),
+ 'consumes', COALESCE(v_consume_dist, '[]'::jsonb)
+ );
+END;
+$$;
+
+GRANT EXECUTE ON FUNCTION public.rpc_admin_get_integral_stats(timestamptz, timestamptz) TO authenticated;
diff --git a/pages/mall/admin/distribution/division/agent.uvue b/pages/mall/admin/distribution/division/agent.uvue
index 31286f87..7ccaa7ac 100644
--- a/pages/mall/admin/distribution/division/agent.uvue
+++ b/pages/mall/admin/distribution/division/agent.uvue
@@ -4,7 +4,7 @@
代理商查询:
-
+
@@ -16,51 +16,119 @@
+
+
+ 加载中...
+
+
+
+ 暂无代理商数据
+
- {{ item.uid }}
+ {{ item.uid }}
-
+
- {{ item.name }}
- {{ item.ratio }}%
- {{ item.staffCount }}
- {{ item.endTime }}
+ {{ item.name || item.nickname }}
+ {{ item.division_name || '-' }}
+ {{ formatDateTime(item.created_at) }}
-
+
- 编辑
+ 详情
|
- 查看
- |
- 员工
- |
- 删除
+ 删除
+
+
\ No newline at end of file
diff --git a/pages/mall/admin/distribution/division/list.uvue b/pages/mall/admin/distribution/division/list.uvue
index de109dc5..65917580 100644
--- a/pages/mall/admin/distribution/division/list.uvue
+++ b/pages/mall/admin/distribution/division/list.uvue
@@ -4,7 +4,7 @@
搜索:
-
+
@@ -16,6 +16,11 @@
+
+
+ 加载中...
+
+
+
+ 暂无事业部数据
+
- {{ item.uid }}
+ {{ item.uid }}
-
+
- {{ item.name }}
- {{ item.code }}
+ {{ item.name || item.nickname }}
+ {{ item.invite_code }}
{{ item.ratio }}%
- {{ item.agentCount }}
- {{ item.endTime }}
+ {{ item.agent_count }}
+ {{ formatTime(item.end_time) }}
-
+
查看代理商
|
- 编辑
- |
- 删除
+ 删除
+
+
diff --git a/pages/mall/admin/marketing/checkin/config.uvue b/pages/mall/admin/marketing/checkin/config.uvue
index c5c1443b..f300a40c 100644
--- a/pages/mall/admin/marketing/checkin/config.uvue
+++ b/pages/mall/admin/marketing/checkin/config.uvue
@@ -6,6 +6,11 @@
+
+
+ 加载中...
+
+
签到开关:
@@ -88,7 +93,7 @@
@@ -96,7 +101,11 @@
@@ -124,7 +177,7 @@ const handleSave = () => {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
-.config-card { padding: 24px; }
+.config-card { padding: 24px; position: relative; }
.config-header {
border-bottom: 1px solid #e8eaec;
@@ -172,7 +225,7 @@ const handleSave = () => {
.config-input {
width: 400px;
- height: 36px;
+ height: 32px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
@@ -186,8 +239,8 @@ const handleSave = () => {
.btn-submit {
width: 80px;
- height: 36px;
- line-height: 36px;
+ height: 32px;
+ line-height: 32px;
background: #1890ff;
color: #fff;
border: none;
@@ -195,4 +248,15 @@ const handleSave = () => {
font-size: 14px;
cursor: pointer;
}
+
+.loading-mask {
+ position: absolute;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(255, 255, 255, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10;
+}
+.loading-text { color: #1890ff; font-size: 14px; }
diff --git a/pages/mall/admin/marketing/combination/list.uvue b/pages/mall/admin/marketing/combination/list.uvue
index 3a71665e..81a8658c 100644
--- a/pages/mall/admin/marketing/combination/list.uvue
+++ b/pages/mall/admin/marketing/combination/list.uvue
@@ -25,7 +25,7 @@
👥
- 349
+ {{ statParticipants }}
参与人数(人)
@@ -34,7 +34,7 @@
📦
- 44
+ {{ statSuccessGroups }}
成团数量(个)
@@ -42,6 +42,10 @@
+
+
+ 数据加载中...
+
头像
开团团长
@@ -55,15 +59,18 @@
+
+ 暂无拼团记录
+
-
+
- {{ item.nickname }} / {{ item.uid }}
+ {{ item.nickname || '未知用户' }} / {{ item.uid }}
- {{ item.start_time }}
+ {{ item.start_time.substring(0, 16).replace('T', ' ') }}
{{ item.title }} / {{ item.cid }}
@@ -75,7 +82,7 @@
{{ item.count_people }}
- {{ item.stop_time }}
+ {{ item.stop_time.substring(0, 16).replace('T', ' ') }}
@@ -86,7 +93,7 @@
查看详情
|
- 立即成团
+ 立即成团
@@ -95,23 +102,12 @@
@@ -119,114 +115,99 @@
diff --git a/pages/mall/admin/marketing/coupon/receive.uvue b/pages/mall/admin/marketing/coupon/receive.uvue
index 9b009092..3654a35c 100644
--- a/pages/mall/admin/marketing/coupon/receive.uvue
+++ b/pages/mall/admin/marketing/coupon/receive.uvue
@@ -1,65 +1,246 @@
-
-
+
+
+
+
+
+ 搜索记录:
+
+
+
+
+
+
+
+
-
- 页面参数(query)
- {{ params }}
+
+
+
+
+ 加载中...
+
+
+
+
+
+
+ 暂无领取记录
+
+
+
+ {{ item.id }}
+
+
+
+
+ {{ item.nickname || '未知用户' }}
+
+
+
+ {{ item.coupon_name || '已失效优惠券' }}
+
+
+ {{ item.coupon_code }}
+
+
+
+ {{ getStatusLabel(item.status) }}
+
+
+
+ {{ formatDateTime(item.received_at) }}
+
+
+ {{ formatDateTime(item.used_at) }}
+
+
+
+
+
+
+
+
-
-
+
+
-
diff --git a/pages/mall/admin/marketing/integral/statistic.uvue b/pages/mall/admin/marketing/integral/statistic.uvue
index f5e02669..fdfbb39f 100644
--- a/pages/mall/admin/marketing/integral/statistic.uvue
+++ b/pages/mall/admin/marketing/integral/statistic.uvue
@@ -5,10 +5,12 @@
时间选择:
-
- 📅
- 2026/01/05 - 2026/02/03
-
+
@@ -19,7 +21,7 @@
💠
- 744904340.25
+ {{ statsTotal.current.toFixed(2) }}
当前积分
@@ -28,7 +30,7 @@
🪙
- 59026484
+ {{ statsTotal.income.toFixed(2) }}
累计总积分
@@ -37,7 +39,7 @@
💎
- 3189
+ {{ statsTotal.expend.toFixed(2) }}
累计消耗积分
@@ -141,43 +143,102 @@
diff --git a/pages/mall/admin/marketing/live/product.uvue b/pages/mall/admin/marketing/live/product.uvue
index 06e85f07..e134a788 100644
--- a/pages/mall/admin/marketing/live/product.uvue
+++ b/pages/mall/admin/marketing/live/product.uvue
@@ -112,73 +112,75 @@
-
-
-
diff --git a/pages/mall/admin/marketing/member/config.uvue b/pages/mall/admin/marketing/member/config.uvue
index 1247f77b..5fbdd900 100644
--- a/pages/mall/admin/marketing/member/config.uvue
+++ b/pages/mall/admin/marketing/member/config.uvue
@@ -69,21 +69,72 @@
diff --git a/pages/mall/admin/marketing/member/right.uvue b/pages/mall/admin/marketing/member/right.uvue
index c877fce4..48830ab7 100644
--- a/pages/mall/admin/marketing/member/right.uvue
+++ b/pages/mall/admin/marketing/member/right.uvue
@@ -2,6 +2,11 @@
+
+
+ 加载中...
+
+
ID
权益图标
@@ -13,20 +18,23 @@
+
+ 暂无权益配置
+
{{ item.id }}
-
+
{{ item.name }}
- {{ item.desc }}
+ {{ item.description || '-' }}
{{ item.is_show ? '显示' : '隐藏' }}
- {{ item.sort }}
+ {{ item.sort_order }}
编辑
@@ -38,20 +46,39 @@
@@ -136,6 +163,31 @@ const handleEdit = (item: any) => {
.switch-mock.active .switch-txt { margin-left: 4px; }
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
+
+/* Loading & Empty Styles */
+.table-container {
+ position: relative;
+ min-height: 300px;
+}
+
+.loading-mask {
+ position: absolute;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10;
+}
+
+.loading-text { color: #1890ff; font-size: 14px; }
+
+.empty-row {
+ padding: 60px 0;
+ text-align: center;
+ color: #999;
+ font-size: 14px;
+}
diff --git a/pages/mall/admin/marketing/member/type.uvue b/pages/mall/admin/marketing/member/type.uvue
index b4cd25ac..a24a7517 100644
--- a/pages/mall/admin/marketing/member/type.uvue
+++ b/pages/mall/admin/marketing/member/type.uvue
@@ -13,22 +13,44 @@
操作
-
-
- {{ item.id }}
- {{ item.name }}
- {{ item.days }}
- ¥{{ item.price.toFixed(2) }}
- ¥{{ item.discount.toFixed(2) }}
-
-
-
- {{ item.is_open ? '开启' : '关闭' }}
-
+
+
+
+
+ 数据加载中...
+
+
+
+
+
+
+ 暂无会员类型配置
- {{ item.sort }}
-
- 编辑
+
+ {{ item.id }}
+ {{ item.name }}
+ {{ item.duration_days == 0 ? '永久' : item.duration_days }}
+ ¥{{ item.price.toFixed(2) }}
+ ¥{{ item.discount_price.toFixed(2) }}
+
+
+
+ {{ item.is_open ? '开启' : '关闭' }}
+
+
+ {{ item.sort_order }}
+
+ 编辑
+
@@ -38,22 +60,39 @@
@@ -141,6 +180,30 @@ const handleEdit = (item: any) => {
.op-link { color: #1890ff; font-size: 13px; cursor: pointer; }
+/* Loading & Empty Styles */
+.table-container {
+ position: relative;
+ min-height: 300px;
+}
+
+.loading-mask {
+ position: absolute;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10;
+}
+
+.loading-text { color: #1890ff; font-size: 14px; }
+
+.empty-row {
+ padding: 60px 0;
+ text-align: center;
+ color: #999;
+ font-size: 14px;
+}
diff --git a/pages/mall/admin/marketing/newcomer/index.uvue b/pages/mall/admin/marketing/newcomer/index.uvue
index ec3641ec..b5f5568d 100644
--- a/pages/mall/admin/marketing/newcomer/index.uvue
+++ b/pages/mall/admin/marketing/newcomer/index.uvue
@@ -45,13 +45,13 @@
-
+
-
+
@@ -65,8 +65,13 @@
-
-
+
+
+ 加载中...
+
+
+
+
显示名称:
@@ -75,13 +80,13 @@
发放描述:
-
- * 此处的修改仅影响“新人礼”活动中的展示,不影响优惠券自身配置
-
+
+ 暂无可用优惠券
+
@@ -108,36 +113,73 @@
@@ -516,6 +568,22 @@ function handleSubmit() {
color: #fff;
border: none;
}
+
+.modal-loading {
+ padding: 40px 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #1890ff;
+ font-size: 14px;
+}
+
+.empty-tip {
+ padding: 40px 0;
+ text-align: center;
+ color: #999;
+ font-size: 14px;
+}
diff --git a/pages/mall/admin/marketing/points/record.uvue b/pages/mall/admin/marketing/points/record.uvue
index 8455baa8..60cf4b1f 100644
--- a/pages/mall/admin/marketing/points/record.uvue
+++ b/pages/mall/admin/marketing/points/record.uvue
@@ -1,27 +1,219 @@
-
-
-
+.marketing-points-record {
+ min-height: 100vh;
+ background: #f0f2f5;
+ padding: 16px;
+}
+.border-shadow {
+ background: #fff;
+ border-radius: 4px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+}
+.filter-card { padding: 24px; margin-bottom: 16px; }
+.filter-row { display: flex; flex-direction: row; align-items: center; gap: 24px; }
+.filter-item { display: flex; flex-direction: row; align-items: center; }
+.label { font-size: 14px; color: #606266; }
+.input-mock {
+ width: 240px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px;
+ padding: 0 12px; font-size: 13px;
+}
+.filter-btns { display: flex; flex-direction: row; gap: 12px; }
+.btn-query { background: #1890ff; color: #fff; height: 32px; padding: 0 16px; border-radius: 4px; font-size: 14px; border: none; cursor: pointer; }
+.btn-reset { background: #fff; color: #666; height: 32px; padding: 0 16px; border-radius: 4px; font-size: 14px; border: 1px solid #dcdfe6; cursor: pointer; }
+
+.table-card { padding: 24px; position: relative; }
+.table-container { min-height: 400px; position: relative; }
+.loading-mask {
+ position: absolute; top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(255,255,255,0.7); display: flex; align-items: center; justify-content: center; z-index: 10;
+}
+.loading-text { color: #1890ff; font-size: 14px; }
+
+.table-header {
+ display: flex; flex-direction: row; background-color: #f8faff;
+ border-bottom: 1px solid #e8eaec;
+}
+.th { padding: 12px 8px; font-size: 13px; color: #515a6e; font-weight: bold; text-align: center; }
+
+.table-row {
+ display: flex; flex-direction: row; border-bottom: 1px solid #e8eaec; align-items: center;
+ &:hover { background-color: #fafafa; }
+}
+.td { padding: 12px 8px; text-align: center; display: flex; align-items: center; justify-content: center; }
+.td-txt { font-size: 13px; color: #515a6e; }
+.td-txt-bold { font-size: 14px; color: #333; font-weight: bold; }
+.td-txt-small { font-size: 12px; color: #999; }
+.color-blue { color: #1890ff; }
+.color-red { color: #f5222d; }
+
+.cell-id { width: 60px; }
+.cell-user { width: 180px; justify-content: flex-start; }
+.cell-title { flex: 1; min-width: 150px; }
+.cell-points { width: 100px; }
+.cell-balance { width: 120px; }
+.cell-time { width: 150px; }
+
+.u-info { display: flex; flex-direction: row; align-items: center; gap: 8px; }
+.u-avatar { width: 32px; height: 32px; border-radius: 16px; background: #f5f5f5; }
+.u-nick { font-size: 13px; color: #333; }
+
+.pagination-footer {
+ margin-top: 24px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px;
+}
+.total-txt { font-size: 13px; color: #999; }
+.page-btns { display: flex; flex-direction: row; gap: 8px; }
+.p-btn {
+ width: 28px; height: 28px; border: 1px solid #dcdfe6; border-radius: 4px;
+ display: flex; align-items: center; justify-content: center; font-size: 14px;
+ &.active { background: #1890ff; color: #fff; border-color: #1890ff; }
+ &.disabled { opacity: 0.5; cursor: not-allowed; }
+}
+.empty-row { padding: 60px 0; text-align: center; color: #999; }
+
\ No newline at end of file
diff --git a/pages/mall/admin/marketing/recharge/config.uvue b/pages/mall/admin/marketing/recharge/config.uvue
index 0ccbf32b..548103ea 100644
--- a/pages/mall/admin/marketing/recharge/config.uvue
+++ b/pages/mall/admin/marketing/recharge/config.uvue
@@ -73,17 +73,63 @@
diff --git a/pages/mall/admin/marketing/recharge/quota.uvue b/pages/mall/admin/marketing/recharge/quota.uvue
index fb29e37b..e1b45e64 100644
--- a/pages/mall/admin/marketing/recharge/quota.uvue
+++ b/pages/mall/admin/marketing/recharge/quota.uvue
@@ -120,74 +120,120 @@
diff --git a/pages/mall/admin/marketing/seckill/list.uvue b/pages/mall/admin/marketing/seckill/list.uvue
index b84a1bda..76aa0349 100644
--- a/pages/mall/admin/marketing/seckill/list.uvue
+++ b/pages/mall/admin/marketing/seckill/list.uvue
@@ -4,19 +4,12 @@
活动搜索:
-
+
活动状态:
- 请选择
- ▼
-
-
-
- 活动时段:
-
- 请选择
+ 全部
▼
@@ -26,10 +19,10 @@
活动时间:
📅
- 开始日期 - 结束日期
+ 暂时使用默认范围
-
+
@@ -39,19 +32,26 @@
+
+
+ 加载中...
+
ID
活动标题
单次限购
- 总购买数量限制
- 商品数量
- 活动时段
- 活动时间
+ 总限购
+ 商品数
+ 时段
+ 活动日期
状态
操作
+
+ 暂无数据
+
{{ item.id }}
@@ -74,8 +74,8 @@
- 开始: {{ item.start_date }}
- 结束: {{ item.end_date }}
+ {{ item.start_date.substring(0,10) }}
+ {{ item.end_date.substring(0,10) }}
@@ -96,23 +96,12 @@
@@ -120,47 +109,90 @@
diff --git a/pages/mall/admin/marketing/signin/rule.uvue b/pages/mall/admin/marketing/signin/rule.uvue
index 229e297e..429c4146 100644
--- a/pages/mall/admin/marketing/signin/rule.uvue
+++ b/pages/mall/admin/marketing/signin/rule.uvue
@@ -1,65 +1,276 @@
-
-
+
+
-
- 页面参数(query)
- {{ params }}
+
+
+
+ 是否开启:
+
+ form.is_enabled = e.detail.value" />
+ 开启后,用户可在移动端进行每日签到获取积分
+
+
+
+
+
+ 每日签到积分:
+
+
+ 用户每天签到可获得的固定积分奖励
+
+
+
+
+
+ 连续签到奖励:
+
+
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+
+ + 添加阶梯奖励
+
+
+ 配置连续签到奖励,例如:连续 3 天额外奖励 20 积分
+
+
+
+
+
+ 规则说明:
+
+
+ 显示在移动端签到页面的说明文字
+
+
+
+
+
+
-
-
+
+
-
diff --git a/pages/mall/admin/product/product-management/edit.uvue b/pages/mall/admin/product/product-management/edit.uvue
index 11523e77..0c7107bb 100644
--- a/pages/mall/admin/product/product-management/edit.uvue
+++ b/pages/mall/admin/product/product-management/edit.uvue
@@ -180,11 +180,75 @@
-
-
-
- 步骤 {{ steps[activeStep] }} 逻辑接入中...
- 该模块涉及物流模板、会员等级定价等,将在后续阶段补齐
+
+
+
+ *运费模板:
+
+
+
+
+ {{ shippingLabels[shippingIndex] }}
+
+ {{ form.shipping_template_id ? '点击切换模板' : '选择运费模板' }}
+
+
+
+
+
+
+
+
+
+ 会员价设置:
+
+
+ 点击上方按钮,可以为不同规格的商品设置各级会员的专属价格。
+
+
+
+
+
+
+
+ 赠送积分:
+
+
+
+
+ 用户购买该商品后赠送的积分数量
+
+
+
+
+
+
+
+ 排序权重:
+
+
+
+
+ 数字越大,商品在列表中越靠前
+
+
+
+ 虚拟销量:
+
+
+
+
+ 显示的销量 = 真实销量 + 虚拟销量
+
+
+
+ 库存预警:
+
+
+
+
+ 当商品总库存低于此数值时,管理后台将进行提醒
+
@@ -229,8 +293,10 @@ import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
import {
fetchAdminProductDetail,
saveAdminProduct,
+ fetchShippingTemplates,
AdminProduct,
- AdminProductSku
+ AdminProductSku,
+ ShippingTemplate
} from '@/services/admin/productService.uts'
import { fetchAdminCategoryList, AdminCategory } from '@/services/admin/productCategoryService.uts'
import { fetchLabels, ProductLabel } from '@/services/admin/productLabelService.uts'
@@ -240,6 +306,7 @@ import { fetchSpecTemplates, fetchParamTemplates, ProductSpecTemplate, ProductPa
const activeStep = ref(0)
const steps = ['基础信息', '规格库存', '商品详情', '物流设置', '会员价/佣金', '营销设置', '其他设置']
const isLoading = ref(false)
+const isSaving = ref(false)
const productId = ref(null)
// 响应式表单数据
@@ -256,7 +323,13 @@ const form = reactive({
price: 0,
stock: 0,
product_code: '',
- attributes: {} as any
+ attributes: {} as any,
+ // 扩展字段
+ shipping_template_id: null as string | null,
+ give_integral: 0,
+ stock_warning: 10,
+ virtual_sales: 0,
+ sort_order: 0
})
const skus = ref([])
@@ -266,10 +339,12 @@ const categories = ref([])
const labelList = ref([])
const specTemplates = ref([])
const paramTemplates = ref([])
+const shippingTemplates = ref([])
// --- UI 控制状态 ---
const showLabelModal = ref(false)
const categoryIndex = ref(0)
+const shippingIndex = ref(-1)
// 计算属性:分类名称列表供 picker 使用
const categoryLabels = computed((): string[] => {
@@ -287,6 +362,11 @@ const specTemplateLabels = computed((): string[] => {
return specTemplates.value.map(t => t.name)
})
+// 计算属性:运费模板列表
+const shippingLabels = computed((): string[] => {
+ return shippingTemplates.value.map(t => t.name)
+})
+
onMounted(() => {
// 1. 获取商品 ID
const pages = getCurrentPages()
@@ -305,16 +385,18 @@ onMounted(() => {
async function initBaseData() {
try {
- const [cateRes, labelRes, specRes, paramRes] = await Promise.all([
+ const [cateRes, labelRes, specRes, paramRes, shipRes] = await Promise.all([
fetchAdminCategoryList({ isActive: true }),
fetchLabels(),
fetchSpecTemplates(),
- fetchParamTemplates()
+ fetchParamTemplates(),
+ fetchShippingTemplates()
])
categories.value = cateRes
labelList.value = labelRes
specTemplates.value = specRes
paramTemplates.value = paramRes
+ shippingTemplates.value = shipRes
} catch (e) {
console.error('加载基础资料失败:', e)
}
@@ -340,6 +422,18 @@ async function loadProductDetail(id: string) {
form.product_code = p.product_code ?? ''
form.attributes = p.attributes ?? {}
+ // 扩展字段
+ form.shipping_template_id = p.shipping_template_id
+ form.give_integral = p.give_integral
+ form.stock_warning = p.stock_warning
+ form.virtual_sales = p.virtual_sales
+ form.sort_order = p.sort_order
+
+ // 匹配运费模板索引
+ if (p.shipping_template_id != null) {
+ shippingIndex.value = shippingTemplates.value.findIndex(t => t.id === p.shipping_template_id)
+ }
+
skus.value = res.skus
}
} catch (e) {
@@ -367,10 +461,25 @@ function prevStep() {
}
}
+function onShippingChange(e : any) {
+ const index = parseInt(String(e.detail.value))
+ shippingIndex.value = index
+ form.shipping_template_id = shippingTemplates.value[index].id
+}
+
+function goMemberPrice() {
+ if (productId.value == null) {
+ uni.showToast({ title: '请先保存商品基础信息', icon: 'none' })
+ return
+ }
+ openRoute('product_member_price', { id: productId.value })
+}
+
async function handleSave() {
- isLoading.value = true
+ if (isSaving.value) return
+ isSaving.value = true
try {
- const productData: Partial = {
+ const productData : Partial = {
id: productId.value ?? undefined,
...form
}
@@ -384,7 +493,7 @@ async function handleSave() {
} catch (e) {
uni.showToast({ title: '系统异常', icon: 'none' })
} finally {
- isLoading.value = false
+ isSaving.value = false
}
}
diff --git a/pages/mall/admin/setting/delivery/template.uvue b/pages/mall/admin/setting/delivery/template.uvue
index f3d8952b..86663355 100644
--- a/pages/mall/admin/setting/delivery/template.uvue
+++ b/pages/mall/admin/setting/delivery/template.uvue
@@ -5,7 +5,7 @@
搜索:
-
+
@@ -16,69 +16,189 @@
+
+
+ 加载中...
+
+
+
+ 暂无运费模板数据
+
- {{ item.id }}
+ {{ item.id }}
{{ item.name }}
- {{ item.method }}
- {{ item.freeShipping ? '开启' : '关闭' }}
- {{ item.sort }}
- {{ item.addTime }}
+ {{ getCalcMethodName(item.calc_method) }}
+ {{ item.is_free_shipping ? '是' : '否' }}
+ {{ item.sort_order }}
- 修改
+ 编辑
删除
+
+
+
+
+
+
+
+ 模板名称:
+
+
+
+ 计费方式:
+ {
+ const methods = ['piece', 'weight', 'volume']
+ editForm.calc_method = methods[e.detail.value as number]
+ }">
+ {{ getCalcMethodName(editForm.calc_method ?? 'piece') }}
+
+
+
+ 是否包邮:
+ editForm.is_free_shipping = e.detail.value" />
+
+
+ 排序:
+
+
+
+
+
+
@@ -184,4 +304,100 @@ function onDel(item: FreightItem) {
color: #ed4014;
font-size: 13px;
}
+
+/* Loading & Empty Styles */
+.table-wrap {
+ position: relative;
+ min-height: 300px;
+}
+.loading-mask {
+ position: absolute;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background-color: rgba(255, 255, 255, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10;
+}
+.loading-text { color: #1890ff; font-size: 14px; }
+.empty-row {
+ padding: 60px 0;
+ text-align: center;
+ color: #999;
+ font-size: 14px;
+}
+.td-txt-small { font-size: 12px; color: #999; }
+
+/* Modal Styles */
+.modal-mask {
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+.modal-content {
+ width: 500px;
+ background-color: #fff;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+}
+.modal-header {
+ padding: 16px 20px;
+ border-bottom: 1px solid #f0f0f0;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+}
+.modal-title { font-size: 16px; font-weight: bold; }
+.modal-close { font-size: 24px; color: #ccc; cursor: pointer; }
+.modal-body { padding: 24px; }
+.modal-footer {
+ padding: 16px 20px;
+ border-top: 1px solid #f0f0f0;
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ gap: 12px;
+}
+
+.form-item {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ margin-bottom: 20px;
+}
+.form-label { width: 100px; font-size: 14px; color: #666; }
+.form-input {
+ flex: 1;
+ height: 36px;
+ border: 1px solid #dcdfe6;
+ border-radius: 4px;
+ padding: 0 12px;
+ font-size: 14px;
+}
+.picker-box {
+ flex: 1;
+ height: 36px;
+ line-height: 34px;
+ border: 1px solid #dcdfe6;
+ border-radius: 4px;
+ padding: 0 12px;
+ font-size: 14px;
+ color: #333;
+}
+
+.btn-cancel {
+ background-color: #fff;
+ color: #666;
+ border: 1px solid #dcdfe6;
+ padding: 0 20px;
+ height: 32px;
+ border-radius: 4px;
+ font-size: 14px;
+}
diff --git a/services/admin/distributionService.uts b/services/admin/distributionService.uts
index ef61851e..7e4906a3 100644
--- a/services/admin/distributionService.uts
+++ b/services/admin/distributionService.uts
@@ -197,3 +197,165 @@ export async function getPromoterList(params?: PromoterListParams): Promise {
+ let q = supa.from('ak_distribution_divisions').select('*, ak_users!uid(username, avatar_url)', { count: 'exact' })
+
+ if (query?.search != null && query.search !== '') {
+ q = q.or(`name.ilike.%${query.search}%,ak_users.username.ilike.%${query.search}%`)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('created_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取事业部列表失败:', error)
+ return { total: 0, items: [] as DistributionDivision[] }
+ }
+
+ const items = (data ?? []).map((item: any): DistributionDivision => {
+ return {
+ ...item,
+ nickname: item.ak_users?.username,
+ avatar_url: item.ak_users?.avatar_url
+ } as DistributionDivision
+ })
+
+ return { total: count ?? 0, items }
+}
+
+/**
+ * 保存事业部(新增/更新)
+ */
+export async function saveDivision(division: DistributionDivision): Promise {
+ const session = supa.getSession()
+ const mid = session?.user?.getString('id')
+ if (mid == null) return false
+
+ const { error } = await supa
+ .from('ak_distribution_divisions')
+ .upsert({
+ ...division,
+ merchant_id: mid,
+ updated_at: new Date().toISOString()
+ })
+ .execute()
+
+ return error == null
+}
+
+/**
+ * 删除事业部
+ */
+export async function deleteDivision(id: string): Promise {
+ const { error } = await supa.from('ak_distribution_divisions').delete().eq('id', id).execute()
+ return error == null
+}
+
+/**
+ * 获取代理商列表
+ */
+export async function fetchAgents(query?: { search?: string, divisionId?: string, page?: number, pageSize?: number }): Promise<{ total: number, items: DistributionAgent[] }> {
+ let q = supa.from('ak_distribution_agents').select('*, ak_users!uid(username, avatar_url), ak_distribution_divisions!division_id(name)', { count: 'exact' })
+
+ if (query?.search != null && query.search !== '') {
+ q = q.or(`name.ilike.%${query.search}%,ak_users.username.ilike.%${query.search}%`)
+ }
+ if (query?.divisionId != null) {
+ q = q.eq('division_id', query.divisionId)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('created_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取代理商列表失败:', error)
+ return { total: 0, items: [] as DistributionAgent[] }
+ }
+
+ const items = (data ?? []).map((item: any): DistributionAgent => {
+ return {
+ ...item,
+ nickname: item.ak_users?.username,
+ avatar_url: item.ak_users?.avatar_url,
+ division_name: item.ak_distribution_divisions?.name
+ } as DistributionAgent
+ })
+
+ return { total: count ?? 0, items }
+}
+
+/**
+ * 保存代理商
+ */
+export async function saveAgent(agent: DistributionAgent): Promise {
+ const session = supa.getSession()
+ const mid = session?.user?.getString('id')
+ if (mid == null) return false
+
+ const { error } = await supa
+ .from('ak_distribution_agents')
+ .upsert({
+ ...agent,
+ merchant_id: mid,
+ updated_at: new Date().toISOString()
+ })
+ .execute()
+
+ return error == null
+}
diff --git a/services/admin/marketingService.uts b/services/admin/marketingService.uts
index 6ec83d8e..22486835 100644
--- a/services/admin/marketingService.uts
+++ b/services/admin/marketingService.uts
@@ -1,3 +1,4 @@
+import { rpcOrValue } from '@/services/analytics/rpc.uts'
import supa from '@/components/supadb/aksupainstance.uts'
/**
@@ -26,6 +27,32 @@ export type CouponTemplate = {
updated_at?: string
}
+/**
+ * 积分统计模型
+ */
+export type IntegralStats = {
+ totals: {
+ current: number
+ income: number
+ expend: number
+ }
+ trend: Array<{
+ date_group: string
+ income: number
+ expend: number
+ }>
+ sources: Array<{
+ label: string
+ value: number
+ percent: number
+ }>
+ consumes: Array<{
+ label: string
+ value: number
+ percent: number
+ }>
+}
+
export type CouponQuery = {
name?: string | null
type?: number | null
@@ -80,6 +107,18 @@ export async function fetchAdminCoupons(query?: CouponQuery): Promise<{ total: n
}
}
+/**
+ * 获取积分统计概况
+ */
+export async function fetchIntegralStats(startTime: string, endTime: string): Promise {
+ const res = await rpcOrValue('rpc_admin_get_integral_stats', {
+ p_start_time: startTime,
+ p_end_time: endTime
+ } as any)
+
+ return res as IntegralStats | null
+}
+
/**
* 保存优惠券模板(新增/更新)
*/
@@ -103,6 +142,69 @@ export async function saveCouponTemplate(tpl: CouponTemplate): Promise
return true
}
+/**
+ * 签到奖励项
+ */
+export type SignInReward = {
+ day : number
+ points : number
+}
+
+/**
+ * 签到配置模型
+ */
+export type SignInConfig = {
+ id ?: string
+ merchant_id ?: string
+ is_enabled : boolean
+ daily_points : number
+ continuous_rewards : SignInReward[]
+ rules_description : string
+ updated_at ?: string
+}
+
+/**
+ * 获取签到规则配置
+ */
+export async function fetchSignInConfig() : Promise {
+ const { data, error } = await supa
+ .from('ak_signin_configs')
+ .select('*')
+ .eq('id', 'signin_config')
+ .single()
+ .execute()
+
+ if (error != null) {
+ // 如果是 406 或不存在,返回 null 由页面处理初始化
+ return null
+ }
+ return data as SignInConfig | null
+}
+
+/**
+ * 保存签到规则配置
+ */
+export async function saveSignInConfig(config : SignInConfig) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+
+ const { error } = await supa
+ .from('ak_signin_configs')
+ .upsert({
+ ...config,
+ id: 'signin_config',
+ merchant_id: uid,
+ updated_at: new Date().toISOString()
+ })
+ .execute()
+
+ if (error != null) {
+ console.error('保存签到配置失败:', error)
+ return false
+ }
+ return true
+}
+
/**
* 切换优惠券状态
*/
@@ -121,6 +223,1010 @@ export async function toggleCouponStatus(id: string, isOpen: boolean): Promise {
+ let q = supa.from('ak_seckill_activities').select('*', { count: 'exact' })
+ if (query?.search != null && query.search !== '') {
+ q = q.ilike('title', `%${query.search}%`)
+ }
+ if (query?.status != null) {
+ q = q.eq('status', query.status)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('created_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取秒杀列表失败:', error)
+ return { total: 0, items: [] as SeckillActivity[] }
+ }
+ return { total: count ?? 0, items: (data ?? []) as SeckillActivity[] }
+}
+
+/**
+ * 保存秒杀活动
+ */
+export async function saveSeckillActivity(activity : SeckillActivity) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_seckill_activities')
+ .upsert({ ...activity, merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 删除秒杀活动
+ */
+export async function deleteSeckillActivity(id : string) : Promise {
+ const { error } = await supa.from('ak_seckill_activities').delete().eq('id', id).execute()
+ return error == null
+}
+
+/**
+ * 获取拼团活动列表
+ */
+export async function fetchCombinationActivities(query ?: { status ?: string, page ?: number, pageSize ?: number }) : Promise<{ total : number, items : CombinationActivity[] }> {
+ // 关联用户和商品信息展示
+ let q = supa.from('ak_combination_activities').select('*, ak_users!uid(username, avatar_url), ml_products!product_id(name, cid)', { count: 'exact' })
+ if (query?.status != null && query.status !== '') {
+ q = q.eq('status', query.status)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('created_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取拼团列表失败:', error)
+ return { total: 0, items: [] as CombinationActivity[] }
+ }
+
+ const items = (data ?? []).map((item : any) : CombinationActivity => {
+ return {
+ ...item,
+ nickname: item.ak_users?.username,
+ avatar: item.ak_users?.avatar_url,
+ title: item.ml_products?.name,
+ cid: item.ml_products?.cid
+ } as CombinationActivity
+ })
+
+ return { total: count ?? 0, items }
+}
+
+/**
+ * 获取拼团统计数据
+ */
+export async function getCombinationStats() : Promise<{ participantCount : number, successCount : number }> {
+ const { data: participants, error: err1 } = await supa.from('ak_combination_activities').select('count_people').execute()
+ const { count: successCount, error: err2 } = await supa.from('ak_combination_activities').select('*', { count: 'exact', head: true }).eq('status', 'ended').execute()
+
+ let totalParticipants = 0
+ if (participants != null) {
+ participants.forEach((p: any) => {
+ totalParticipants += (p.count_people as number)
+ })
+ }
+
+ return {
+ participantCount: totalParticipants,
+ successCount: successCount ?? 0
+ }
+}
+
+/**
+ * 立即成团
+ */
+export async function completeCombinationGroup(id : string) : Promise {
+ const { error } = await supa
+ .from('ak_combination_activities')
+ .update({ status: 'ended', updated_at: new Date().toISOString() })
+ .eq('id', id)
+ .execute()
+ return error == null
+}
+
+/**
+ * 砍价活动模型
+ */
+export type BargainActivity = {
+ id ?: string
+ merchant_id ?: string
+ product_id : string
+ product_name ?: string
+ product_image ?: string
+ title : string
+ min_price : number
+ stock : number
+ start_time : string
+ stop_time : string
+ status : boolean
+ created_at ?: string
+ updated_at ?: string
+}
+
+/**
+ * 团购活动模型
+ */
+export type GroupbuyActivity = {
+ id ?: string
+ merchant_id ?: string
+ product_id : string
+ product_name ?: string
+ product_image ?: string
+ title : string
+ price : number
+ people : number
+ stock : number
+ start_time : string
+ stop_time : string
+ status : boolean
+ created_at ?: string
+ updated_at ?: string
+}
+
+/**
+ * 获取砍价活动列表
+ */
+export async function fetchBargainActivities(query ?: { search ?: string, page ?: number, pageSize ?: number }) : Promise<{ total : number, items : BargainActivity[] }> {
+ let q = supa.from('ak_marketing_bargains').select('*, ml_products!product_id(name, main_image_url)', { count: 'exact' })
+ if (query?.search != null && query.search !== '') {
+ q = q.ilike('title', `%${query.search}%`)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('created_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取砍价列表失败:', error)
+ return { total: 0, items: [] as BargainActivity[] }
+ }
+
+ const items = (data ?? []).map((item : any) : BargainActivity => {
+ return {
+ ...item,
+ product_name: item.ml_products?.name,
+ product_image: item.ml_products?.main_image_url
+ } as BargainActivity
+ })
+
+ return { total: count ?? 0, items }
+}
+
+/**
+ * 保存砍价活动
+ */
+export async function saveBargainActivity(activity : BargainActivity) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_marketing_bargains')
+ .upsert({ ...activity, merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 删除砍价活动
+ */
+export async function deleteBargainActivity(id : string) : Promise {
+ const { error } = await supa.from('ak_marketing_bargains').delete().eq('id', id).execute()
+ return error == null
+}
+
+/**
+ * 获取团购活动列表
+ */
+export async function fetchGroupbuyActivities(query ?: { search ?: string, page ?: number, pageSize ?: number }) : Promise<{ total : number, items : GroupbuyActivity[] }> {
+ let q = supa.from('ak_marketing_groupbuys').select('*, ml_products!product_id(name, main_image_url)', { count: 'exact' })
+ if (query?.search != null && query.search !== '') {
+ q = q.ilike('title', `%${query.search}%`)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('created_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取团购列表失败:', error)
+ return { total: 0, items: [] as GroupbuyActivity[] }
+ }
+
+ const items = (data ?? []).map((item : any) : GroupbuyActivity => {
+ return {
+ ...item,
+ product_name: item.ml_products?.name,
+ product_image: item.ml_products?.main_image_url
+ } as GroupbuyActivity
+ })
+
+ return { total: count ?? 0, items }
+}
+
+/**
+ * 保存团购活动
+ */
+export async function saveGroupbuyActivity(activity : GroupbuyActivity) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_marketing_groupbuys')
+ .upsert({ ...activity, merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 删除团购活动
+ */
+export async function deleteGroupbuyActivity(id : string) : Promise {
+ const { error } = await supa.from('ak_marketing_groupbuys').delete().eq('id', id).execute()
+ return error == null
+}
+
+/**
+ * 抽奖活动模型
+ */
+export type LotteryActivity = {
+ id ?: string
+ merchant_id ?: string
+ name : string
+ type : number // 1积分抽奖, 2订单评价, 3订单支付
+ start_time : string
+ end_time : string
+ is_open : boolean
+ created_at ?: string
+ updated_at ?: string
+ // 统计字段
+ memberCount ?: number
+ winningMemberCount ?: number
+ lotteryTimes ?: number
+ winningTimes ?: number
+}
+
+/**
+ * 抽奖奖品模型
+ */
+export type LotteryPrize = {
+ id ?: string
+ lottery_id : string
+ name : string
+ prize_type : string // points, balance, coupon, physical
+ amount : number
+ stock : number
+ probability : number
+ sort_order : number
+}
+
+/**
+ * 获取抽奖活动列表
+ */
+export async function fetchLotteryList(query ?: { search ?: string, status ?: number, type ?: number, page ?: number, pageSize ?: number }) : Promise<{ total : number, items : LotteryActivity[] }> {
+ let q = supa.from('ak_marketing_lotteries').select('*', { count: 'exact' })
+
+ if (query?.search != null && query.search !== '') {
+ q = q.ilike('name', `%${query.search}%`)
+ }
+ if (query?.type != null && query.type !== 0) {
+ q = q.eq('type', query.type)
+ }
+ // status 过滤逻辑通常涉及时间判断,此处简化为 is_open
+ if (query?.status != null && query.status !== 0) {
+ q = q.eq('is_open', query.status === 1)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('created_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取抽奖列表失败:', error)
+ return { total: 0, items: [] as LotteryActivity[] }
+ }
+
+ return { total: count ?? 0, items: (data ?? []) as LotteryActivity[] }
+}
+
+/**
+ * 保存抽奖活动
+ */
+export async function saveLotteryActivity(activity : LotteryActivity) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_marketing_lotteries')
+ .upsert({ ...activity, merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 删除抽奖活动
+ */
+export async function deleteLotteryActivity(id : string) : Promise {
+ const { error } = await supa.from('ak_marketing_lotteries').delete().eq('id', id).execute()
+ return error == null
+}
+
+/**
+ * 主播模型
+ */
+export type LiveAnchor = {
+ id ?: string
+ merchant_id ?: string
+ nickname : string
+ wechat : string | null
+ phone : string | null
+ avatar_url : string | null
+ status : boolean
+ created_at ?: string
+}
+
+/**
+ * 直播间模型
+ */
+export type LiveRoom = {
+ id ?: string
+ merchant_id ?: string
+ anchor_id : string | null
+ name : string
+ background_url : string | null
+ share_img_url : string | null
+ start_time : string
+ end_time : string
+ sort : number
+ type : string
+ like_enabled : boolean
+ sale_enabled : boolean
+ comment_enabled : boolean
+ is_show : boolean
+ live_status : number // 1未开始, 2直播中, 3暂停, 4已结束
+ created_at ?: string
+ updated_at ?: string
+ // 关联字段
+ anchor_nick ?: string
+ anchor_wechat ?: string
+}
+
+/**
+ * 获取直播间列表
+ */
+export async function fetchLiveRooms(query ?: { search ?: string, page ?: number, pageSize ?: number }) : Promise<{ total : number, items : LiveRoom[] }> {
+ let q = supa.from('ak_marketing_live_rooms').select('*, ak_marketing_live_anchors(nickname, wechat)', { count: 'exact' })
+
+ if (query?.search != null && query.search !== '') {
+ q = q.or(`name.ilike.%${query.search}%,ak_marketing_live_anchors.nickname.ilike.%${query.search}%`)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('sort', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取直播间列表失败:', error)
+ return { total: 0, items: [] as LiveRoom[] }
+ }
+
+ const items = (data ?? []).map((item : any) : LiveRoom => {
+ return {
+ ...item,
+ anchor_nick: item.ak_marketing_live_anchors?.nickname,
+ anchor_wechat: item.ak_marketing_live_anchors?.wechat
+ } as LiveRoom
+ })
+
+ return { total: count ?? 0, items }
+}
+
+/**
+ * 保存直播间
+ */
+export async function saveLiveRoom(room : LiveRoom) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_marketing_live_rooms')
+ .upsert({ ...room, merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 删除直播间
+ */
+export async function deleteLiveRoom(id : string) : Promise {
+ const { error } = await supa.from('ak_marketing_live_rooms').delete().eq('id', id).execute()
+ return error == null
+}
+
+/**
+ * 获取主播列表
+ */
+export async function fetchLiveAnchors() : Promise {
+ const { data, error } = await supa
+ .from('ak_marketing_live_anchors')
+ .select('*')
+ .eq('status', true)
+ .order('created_at', { ascending: false })
+ .execute()
+
+ if (error != null) return [] as LiveAnchor[]
+ return (data ?? []) as LiveAnchor[]
+}
+
+/**
+ * 保存主播
+ */
+export async function saveLiveAnchor(anchor : LiveAnchor) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_marketing_live_anchors')
+ .upsert({ ...anchor, merchant_id: uid })
+ .execute()
+ return error == null
+}
+
+/**
+ * 删除主播
+ */
+export async function deleteLiveAnchor(id : string) : Promise {
+ const { error } = await supa.from('ak_marketing_live_anchors').delete().eq('id', id).execute()
+ return error == null
+}
+
+/**
+ * 会员卡类型模型
+ */
+export type MemberType = {
+ id ?: string
+ merchant_id ?: string
+ name : string
+ duration_days : number
+ price : number
+ discount_price : number
+ is_open : boolean
+ sort_order : number
+ created_at ?: string
+ updated_at ?: string
+}
+
+/**
+ * 会员权益模型
+ */
+export type MemberRight = {
+ id ?: string
+ merchant_id ?: string
+ name : string
+ description : string | null
+ icon_url : string | null
+ is_show : boolean
+ sort_order : number
+ created_at ?: string
+ updated_at ?: string
+}
+
+/**
+ * 会员基础配置模型
+ */
+export type MemberConfig = {
+ id ?: string
+ merchant_id ?: string
+ is_enabled : boolean
+ bg_img_url : string | null
+ expire_bg_img_url : string | null
+ rules_description : string | null
+ updated_at ?: string
+}
+
+/**
+ * 获取会员类型列表
+ */
+export async function fetchMemberTypes() : Promise {
+ const { data, error } = await supa
+ .from('ak_marketing_member_types')
+ .select('*')
+ .order('sort_order', { ascending: true })
+ .execute()
+
+ if (error != null) {
+ console.error('获取会员类型失败:', error)
+ return [] as MemberType[]
+ }
+ return (data ?? []) as MemberType[]
+}
+
+/**
+ * 保存会员类型
+ */
+export async function saveMemberType(item : MemberType) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_marketing_member_types')
+ .upsert({ ...item, merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 删除会员类型
+ */
+export async function deleteMemberType(id : string) : Promise {
+ const { error } = await supa.from('ak_marketing_member_types').delete().eq('id', id).execute()
+ return error == null
+}
+
+/**
+ * 获取会员权益列表
+ */
+export async function fetchMemberRights() : Promise {
+ const { data, error } = await supa
+ .from('ak_marketing_member_rights')
+ .select('*')
+ .order('sort_order', { ascending: true })
+ .execute()
+
+ if (error != null) {
+ console.error('获取会员权益失败:', error)
+ return [] as MemberRight[]
+ }
+ return (data ?? []) as MemberRight[]
+}
+
+/**
+ * 保存会员权益
+ */
+export async function saveMemberRight(item : MemberRight) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_marketing_member_rights')
+ .upsert({ ...item, merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 删除会员权益
+ */
+export async function deleteMemberRight(id : string) : Promise {
+ const { error } = await supa.from('ak_marketing_member_rights').delete().eq('id', id).execute()
+ return error == null
+}
+
+/**
+ * 获取会员基础配置
+ */
+export async function fetchMemberConfig() : Promise {
+ const { data, error } = await supa
+ .from('ak_marketing_member_config')
+ .select('*')
+ .eq('id', 'member_config')
+ .single()
+ .execute()
+
+ if (error != null) return null
+ return data as MemberConfig | null
+}
+
+/**
+ * 保存会员基础配置
+ */
+export async function saveMemberConfig(config : MemberConfig) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_marketing_member_config')
+ .upsert({ ...config, id: 'member_config', merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 充值基础配置模型
+ */
+export type RechargeConfig = {
+ id ?: string
+ merchant_id ?: string
+ balance_enabled : boolean
+ recharge_notice : string | null
+ mp_recharge_enabled : boolean
+ min_recharge_amount : number
+ updated_at ?: string
+}
+
+/**
+ * 充值额度模板模型
+ */
+export type RechargeQuota = {
+ id ?: string
+ merchant_id ?: string
+ price : number
+ bonus_price : number
+ is_open : boolean
+ sort_order : number
+ created_at ?: string
+ updated_at ?: string
+}
+
+/**
+ * 充值记录模型
+ */
+export type RechargeRecord = {
+ id : string
+ uid : string
+ nickname ?: string
+ avatar ?: string
+ order_no : string
+ recharge_type : string
+ price : number
+ give_price : number
+ paid : number
+ pay_time : string | null
+ created_at : string
+}
+
+/**
+ * 新人礼配置模型
+ */
+export type NewcomerConfig = {
+ id ?: string
+ merchant_id ?: string
+ balance_reward : number
+ integral_reward : number
+ coupons_json : any[]
+ updated_at ?: string
+}
+
+/**
+ * 获取充值基础配置
+ */
+export async function fetchRechargeConfig() : Promise {
+ const { data, error } = await supa
+ .from('ak_recharge_configs')
+ .select('*')
+ .eq('id', 'recharge_config')
+ .single()
+ .execute()
+
+ if (error != null) return null
+ return data as RechargeConfig | null
+}
+
+/**
+ * 获取新人礼配置
+ */
+export async function fetchNewcomerConfig() : Promise {
+ const { data, error } = await supa
+ .from('ak_marketing_newcomer_config')
+ .select('*')
+ .eq('id', 'newcomer_config')
+ .single()
+ .execute()
+
+ if (error != null) return null
+ return data as NewcomerConfig | null
+}
+
+/**
+ * 保存新人礼配置
+ */
+export async function saveNewcomerConfig(config : NewcomerConfig) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_marketing_newcomer_config')
+ .upsert({ ...config, id: 'newcomer_config', merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 签到记录模型
+ */
+export type SignInLog = {
+ id : string
+ uid : string
+ nickname ?: string
+ avatar ?: string
+ points : number
+ is_continuous_reward : boolean
+ created_at : string
+}
+
+/**
+ * 用户优惠券记录模型
+ */
+export type UserCouponRecord = {
+ id : string
+ uid : string
+ nickname ?: string
+ avatar ?: string
+ coupon_id : string
+ coupon_name ?: string
+ coupon_code : string
+ status : number // 1:未使用, 2:已使用, 3:已过期
+ received_at : string
+ used_at : string | null
+}
+
+/**
+ * 获取签到记录列表
+ */
+export async function fetchSignInRecords(query ?: { search ?: string, page ?: number, pageSize ?: number }) : Promise<{ total : number, items : SignInLog[] }> {
+ let q = supa.from('ak_marketing_signin_logs').select('*, ak_users!uid(username, avatar_url)', { count: 'exact' })
+
+ if (query?.search != null && query.search !== '') {
+ q = q.or(`ak_users.username.ilike.%${query.search}%`)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('created_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取签到记录失败:', error)
+ return { total: 0, items: [] as SignInLog[] }
+ }
+
+ const items = (data ?? []).map((item : any) : SignInLog => {
+ return {
+ ...item,
+ nickname: item.ak_users?.username,
+ avatar: item.ak_users?.avatar_url
+ } as SignInLog
+ })
+
+ return { total: count ?? 0, items }
+}
+
+/**
+ * 获取优惠券领取记录列表
+ */
+export async function fetchCouponReceiveRecords(query ?: { search ?: string, page ?: number, pageSize ?: number }) : Promise<{ total : number, items : UserCouponRecord[] }> {
+ let q = supa.from('ml_user_coupons').select('*, ak_users!uid(username, avatar_url), ml_coupon_templates!template_id(name)', { count: 'exact' })
+
+ if (query?.search != null && query.search !== '') {
+ q = q.or(`ak_users.username.ilike.%${query.search}%,coupon_code.ilike.%${query.search}%`)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('received_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取优惠券领取记录失败:', error)
+ return { total: 0, items: [] as UserCouponRecord[] }
+ }
+
+ const items = (data ?? []).map((item : any) : UserCouponRecord => {
+ return {
+ ...item,
+ nickname: item.ak_users?.username,
+ avatar: item.ak_users?.avatar_url,
+ coupon_name: item.ml_coupon_templates?.name
+ } as UserCouponRecord
+ })
+
+ return { total: count ?? 0, items }
+}
+
+/**
+ * 获取积分变动记录列表
+ */
+export async function fetchPointsRecords(query ?: { search ?: string, page ?: number, pageSize ?: number }) : Promise<{ total : number, items : any[] }> {
+ let q = supa.from('ml_user_bill').select('*, ak_users!uid(username, avatar_url)', { count: 'exact' })
+ q = q.eq('category', 'integral')
+
+ if (query?.search != null && query.search !== '') {
+ q = q.or(`ak_users.username.ilike.%${query.search}%,title.ilike.%${query.search}%`)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('created_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取积分记录失败:', error)
+ return { total: 0, items: [] as any[] }
+ }
+
+ const items = (data ?? []).map((item : any) : any => {
+ return {
+ ...item,
+ nickname: item.ak_users?.username,
+ avatar: item.ak_users?.avatar_url
+ }
+ })
+
+ return { total: count ?? 0, items }
+}
+
+/**
+ * 保存充值基础配置
+ */
+export async function saveRechargeConfig(config : RechargeConfig) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_recharge_configs')
+ .upsert({ ...config, id: 'recharge_config', merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 获取充值额度列表
+ */
+export async function fetchRechargeQuotas() : Promise {
+ const { data, error } = await supa
+ .from('ak_recharge_quotas')
+ .select('*')
+ .order('sort_order', { ascending: true })
+ .execute()
+
+ if (error != null) return [] as RechargeQuota[]
+ return (data ?? []) as RechargeQuota[]
+}
+
+/**
+ * 保存充值额度
+ */
+export async function saveRechargeQuota(item : RechargeQuota) : Promise {
+ const uid = getCurrentUid()
+ if (uid == null) return false
+ const { error } = await supa
+ .from('ak_recharge_quotas')
+ .upsert({ ...item, merchant_id: uid, updated_at: new Date().toISOString() })
+ .execute()
+ return error == null
+}
+
+/**
+ * 删除充值额度
+ */
+export async function deleteRechargeQuota(id : string) : Promise {
+ const { error } = await supa.from('ak_recharge_quotas').delete().eq('id', id).execute()
+ return error == null
+}
+
+/**
+ * 获取充值记录列表
+ */
+export async function fetchRechargeRecords(query ?: { search ?: string, page ?: number, pageSize ?: number }) : Promise<{ total : number, items : RechargeRecord[] }> {
+ // 关联用户昵称展示
+ let q = supa.from('ml_user_recharge').select('*, ak_users!uid(username, avatar_url)', { count: 'exact' })
+
+ if (query?.search != null && query.search !== '') {
+ q = q.or(`order_no.ilike.%${query.search}%,ak_users.username.ilike.%${query.search}%`)
+ }
+
+ const p = query?.page ?? 1
+ const ps = query?.pageSize ?? 20
+ const from = (p - 1) * ps
+ const to = from + ps - 1
+
+ const { data, error, count } = await q
+ .order('created_at', { ascending: false })
+ .range(from, to)
+ .execute()
+
+ if (error != null) {
+ console.error('获取充值记录失败:', error)
+ return { total: 0, items: [] as RechargeRecord[] }
+ }
+
+ const items = (data ?? []).map((item : any) : RechargeRecord => {
+ return {
+ ...item,
+ nickname: item.ak_users?.username,
+ avatar: item.ak_users?.avatar_url
+ } as RechargeRecord
+ })
+
+ return { total: count ?? 0, items }
+}
+
/**
* 删除优惠券模板
*/
diff --git a/services/admin/productService.uts b/services/admin/productService.uts
index e79651f5..0f5dd0b4 100644
--- a/services/admin/productService.uts
+++ b/services/admin/productService.uts
@@ -1,6 +1,9 @@
import supa from '@/components/supadb/aksupainstance.uts'
import { rpcOrNull, rpcOrValue } from '@/services/analytics/rpc.uts'
+/**
+ * 商品主模型 (SPU)
+ */
export type AdminProduct = {
id: string
name: string
@@ -20,22 +23,95 @@ export type AdminProduct = {
video_urls: string[] | null
tags: string[] | null
attributes: any | null
+ // 扩展字段 (Step 3-6)
+ shipping_template_id: string | null
+ give_integral: number
+ stock_warning: number
+ virtual_sales: number
+ sort_order: number
}
+/**
+ * 商品 SKU 模型
+ */
export type AdminProductSku = {
- id?: string
- product_id?: string
- sku_code: string
- specifications: any
- price: number
- stock: number
- status: number
- image_url: string | null
+ id ?: string
+ product_id ?: string
+ sku_code : string
+ specifications : any
+ price : number
+ stock : number
+ status : number
+ image_url : string | null
+}
+
+/**
+ * 运费模板模型
+ */
+export type ShippingTemplate = {
+ id : string
+ merchant_id ?: string
+ name : string
+ calc_method : string // piece, weight, volume
+ is_free_shipping : boolean
+ sort_order : number
+ created_at ?: string
+ updated_at ?: string
}
export type ProductPageResult = {
- total: number
- items: Array
+ total : number
+ items : Array
+}
+
+/**
+ * 获取运费模板列表
+ */
+export async function fetchShippingTemplates() : Promise {
+ const { data, error } = await supa
+ .from('ak_shipping_templates')
+ .select('*')
+ .order('sort_order', { ascending: true })
+ .execute()
+
+ if (error != null) {
+ console.error('获取运费模板失败:', error)
+ return [] as ShippingTemplate[]
+ }
+ return (data ?? []) as ShippingTemplate[]
+}
+
+/**
+ * 保存运费模板(新增/更新)
+ */
+export async function saveShippingTemplate(tpl : Partial) : Promise {
+ const session = supa.getSession()
+ const uid = session?.user?.getString('id')
+ if (uid == null) return false
+
+ const { error } = await supa
+ .from('ak_shipping_templates')
+ .upsert({
+ ...tpl,
+ merchant_id: uid,
+ updated_at: new Date().toISOString()
+ })
+ .execute()
+
+ return error == null
+}
+
+/**
+ * 删除运费模板
+ */
+export async function deleteShippingTemplate(id : string) : Promise {
+ const { error } = await supa
+ .from('ak_shipping_templates')
+ .delete()
+ .eq('id', id)
+ .execute()
+
+ return error == null
}
/**
@@ -131,8 +207,7 @@ export async function saveAdminProduct(product: Partial, skus: Adm
const savedProductId = (savedProduct as any).id as string
- // 2. 保存 SKU(简单策略:先删除旧的再插入新的,或使用 upsert)
- // 注意:如果涉及订单关联,不能简单的物理删除旧 SKU
+ // 2. 保存 SKU
const skuPayload = skus.map(s => ({
...s,
product_id: savedProductId,