增加推销模式

This commit is contained in:
cyh666666
2026-03-06 17:30:50 +08:00
parent 3b0e397714
commit 7b5801a72b
39 changed files with 9831 additions and 34 deletions

View File

@@ -0,0 +1,260 @@
-- =====================================================
-- 积分与评价功能完善 - 数据库表创建脚本
-- 创建日期: 2026-03-05
-- =====================================================
-- =====================================================
-- 一、积分相关表
-- =====================================================
-- 1. 签到记录表
CREATE TABLE IF NOT EXISTS ml_signin_records (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
signin_date DATE NOT NULL,
points_earned INT DEFAULT 0,
bonus_points INT DEFAULT 0,
continuous_days INT DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, signin_date)
);
CREATE INDEX IF NOT EXISTS idx_signin_records_user_id ON ml_signin_records(user_id);
CREATE INDEX IF NOT EXISTS idx_signin_records_date ON ml_signin_records(signin_date);
COMMENT ON TABLE ml_signin_records IS '用户签到记录表';
-- 2. 积分兑换商品表
CREATE TABLE IF NOT EXISTS ml_point_products (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
image_url VARCHAR(500),
product_type VARCHAR(50) NOT NULL DEFAULT 'coupon',
points_required INT NOT NULL,
original_price DECIMAL(10,2),
stock INT DEFAULT 0,
status INT DEFAULT 1,
sort_order INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE ml_point_products IS '积分兑换商品表';
COMMENT ON COLUMN ml_point_products.product_type IS '类型: coupon=优惠券, physical=实物, virtual=虚拟商品';
-- 3. 积分兑换记录表
CREATE TABLE IF NOT EXISTS ml_point_exchanges (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
product_id UUID NOT NULL REFERENCES ml_point_products(id),
quantity INT DEFAULT 1,
points_used INT NOT NULL,
status INT DEFAULT 0,
tracking_no VARCHAR(100),
address_snapshot JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_point_exchanges_user_id ON ml_point_exchanges(user_id);
CREATE INDEX IF NOT EXISTS idx_point_exchanges_product_id ON ml_point_exchanges(product_id);
COMMENT ON TABLE ml_point_exchanges IS '积分兑换记录表';
COMMENT ON COLUMN ml_point_exchanges.status IS '状态: 0=待处理, 1=已发货, 2=已完成, 3=已取消';
-- 4. 积分规则配置表
CREATE TABLE IF NOT EXISTS ml_point_rules (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
rule_type VARCHAR(50) NOT NULL,
rule_name VARCHAR(100) NOT NULL,
points INT NOT NULL,
description TEXT,
config JSONB,
status INT DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(rule_type)
);
COMMENT ON TABLE ml_point_rules IS '积分规则配置表';
-- 初始化积分规则数据
INSERT INTO ml_point_rules (rule_type, rule_name, points, description) VALUES
('register', '注册赠送', 100, '新用户注册赠送积分'),
('signin_daily', '每日签到', 5, '每日签到获得积分'),
('signin_continuous_7', '连续签到7天奖励', 20, '连续签到7天额外奖励'),
('signin_continuous_30', '连续签到30天奖励', 100, '连续签到30天额外奖励'),
('shopping', '购物奖励', 1, '每消费1元获得1积分'),
('review', '评价奖励', 10, '完成商品评价获得积分'),
('review_with_image', '带图评价奖励', 20, '带图评价额外奖励')
ON CONFLICT (rule_type) DO NOTHING;
-- =====================================================
-- 二、评价相关表
-- =====================================================
-- 1. 为商品评价表添加新字段
ALTER TABLE ml_product_reviews ADD COLUMN IF NOT EXISTS videos JSONB DEFAULT '[]';
ALTER TABLE ml_product_reviews ADD COLUMN IF NOT EXISTS tags JSONB DEFAULT '[]';
ALTER TABLE ml_product_reviews ADD COLUMN IF NOT EXISTS like_count INT DEFAULT 0;
ALTER TABLE ml_product_reviews ADD COLUMN IF NOT EXISTS is_edited BOOLEAN DEFAULT false;
ALTER TABLE ml_product_reviews ADD COLUMN IF NOT EXISTS append_content TEXT;
ALTER TABLE ml_product_reviews ADD COLUMN IF NOT EXISTS append_at TIMESTAMPTZ;
ALTER TABLE ml_product_reviews ADD COLUMN IF NOT EXISTS append_images JSONB DEFAULT '[]';
-- 2. 评价点赞表
CREATE TABLE IF NOT EXISTS ml_review_likes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
review_id UUID NOT NULL REFERENCES ml_product_reviews(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(review_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_review_likes_review_id ON ml_review_likes(review_id);
CREATE INDEX IF NOT EXISTS idx_review_likes_user_id ON ml_review_likes(user_id);
COMMENT ON TABLE ml_review_likes IS '评价点赞表';
-- 3. 评价举报表
CREATE TABLE IF NOT EXISTS ml_review_reports (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
review_id UUID NOT NULL REFERENCES ml_product_reviews(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id),
reason VARCHAR(200) NOT NULL,
description TEXT,
status INT DEFAULT 0,
handle_result TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_review_reports_review_id ON ml_review_reports(review_id);
CREATE INDEX IF NOT EXISTS idx_review_reports_user_id ON ml_review_reports(user_id);
COMMENT ON TABLE ml_review_reports IS '评价举报表';
COMMENT ON COLUMN ml_review_reports.status IS '状态: 0=待处理, 1=已处理, 2=已驳回';
-- 4. 配送员评价表
CREATE TABLE IF NOT EXISTS ml_delivery_ratings (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
order_id UUID NOT NULL REFERENCES ml_orders(id),
delivery_user_id UUID NOT NULL,
user_id UUID NOT NULL REFERENCES auth.users(id),
rating INT CHECK (rating >= 1 AND rating <= 5),
content TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(order_id)
);
CREATE INDEX IF NOT EXISTS idx_delivery_ratings_delivery_user_id ON ml_delivery_ratings(delivery_user_id);
COMMENT ON TABLE ml_delivery_ratings IS '配送员评价表';
-- =====================================================
-- 三、RLS 策略
-- =====================================================
-- 签到记录表 RLS
ALTER TABLE ml_signin_records ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own signin records"
ON ml_signin_records FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own signin records"
ON ml_signin_records FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- 积分兑换商品表 RLS (所有人可查看上架商品)
ALTER TABLE ml_point_products ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view active point products"
ON ml_point_products FOR SELECT
TO authenticated, anon
USING (status = 1);
-- 允许认证用户更新商品库存(用于兑换扣减)
CREATE POLICY "Authenticated users can update point products"
ON ml_point_products FOR UPDATE
TO authenticated
USING (true)
WITH CHECK (true);
-- 积分兑换记录表 RLS
ALTER TABLE ml_point_exchanges ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own exchange records"
ON ml_point_exchanges FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own exchange records"
ON ml_point_exchanges FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- 评价点赞表 RLS
ALTER TABLE ml_review_likes ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view review likes"
ON ml_review_likes FOR SELECT
TO authenticated, anon
USING (true);
CREATE POLICY "Users can insert own likes"
ON ml_review_likes FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own likes"
ON ml_review_likes FOR DELETE
TO authenticated
USING (auth.uid() = user_id);
-- 评价举报表 RLS
ALTER TABLE ml_review_reports ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own reports"
ON ml_review_reports FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert reports"
ON ml_review_reports FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- 配送员评价表 RLS
ALTER TABLE ml_delivery_ratings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view delivery ratings"
ON ml_delivery_ratings FOR SELECT
TO authenticated, anon
USING (true);
CREATE POLICY "Users can insert own delivery ratings"
ON ml_delivery_ratings FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- =====================================================
-- 四、插入测试数据
-- =====================================================
-- 插入测试积分兑换商品
INSERT INTO ml_point_products (name, description, image_url, product_type, points_required, original_price, stock, status, sort_order) VALUES
('满10减5优惠券', '全场通用满10元可用', '', 'coupon', 100, 5.00, 1000, 1, 1),
('满50减10优惠券', '全场通用满50元可用', '', 'coupon', 200, 10.00, 500, 1, 2),
('精美手机支架', '通用手机支架,多色可选', '', 'physical', 500, 15.00, 100, 1, 3),
('会员月卡', '享受会员专属权益30天', '', 'virtual', 1000, 29.90, 999, 1, 4),
('满100减30优惠券', '全场通用满100元可用', '', 'coupon', 500, 30.00, 200, 1, 5),
('品牌保温杯', '304不锈钢保温杯500ml', '', 'physical', 2000, 59.00, 50, 1, 6)
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,356 @@
-- =====================================================
-- 积分过期提醒功能 - 数据库脚本
-- 创建日期: 2026-03-06
-- =====================================================
-- =====================================================
-- 一、更新现有表结构
-- =====================================================
-- 为积分记录表添加过期时间字段(如果不存在)
ALTER TABLE ml_point_records ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;
ALTER TABLE ml_point_records ADD COLUMN IF NOT EXISTS is_expired BOOLEAN DEFAULT FALSE;
ALTER TABLE ml_point_records ADD COLUMN IF NOT EXISTS expired_at TIMESTAMPTZ;
-- 为积分记录表添加余额字段(记录变动后余额)
ALTER TABLE ml_point_records ADD COLUMN IF NOT EXISTS balance_after INT;
-- 添加索引
CREATE INDEX IF NOT EXISTS idx_point_records_expires_at ON ml_point_records(expires_at);
CREATE INDEX IF NOT EXISTS idx_point_records_is_expired ON ml_point_records(is_expired);
-- 更新用户积分表,添加即将过期积分字段
ALTER TABLE ml_user_points ADD COLUMN IF NOT EXISTS expiring_points INT DEFAULT 0;
ALTER TABLE ml_user_points ADD COLUMN IF NOT EXISTS expiring_date DATE;
COMMENT ON COLUMN ml_point_records.expires_at IS '积分过期时间NULL表示永不过期';
COMMENT ON COLUMN ml_point_records.is_expired IS '是否已过期';
COMMENT ON COLUMN ml_point_records.expired_at IS '实际过期时间';
COMMENT ON COLUMN ml_user_points.expiring_points IS '即将过期积分30天内';
COMMENT ON COLUMN ml_user_points.expiring_date IS '最近过期日期';
-- =====================================================
-- 二、创建积分过期通知表
-- =====================================================
CREATE TABLE IF NOT EXISTS ml_point_expiry_notifications (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
points_expiring INT NOT NULL,
expiry_date DATE NOT NULL,
notification_type VARCHAR(20) NOT NULL, -- '7days', '3days', '1day'
is_sent BOOLEAN DEFAULT FALSE,
sent_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_expiry_notifications_user_id ON ml_point_expiry_notifications(user_id);
CREATE INDEX IF NOT EXISTS idx_expiry_notifications_is_sent ON ml_point_expiry_notifications(is_sent);
COMMENT ON TABLE ml_point_expiry_notifications IS '积分过期通知记录表';
-- =====================================================
-- 三、创建积分过期配置表
-- =====================================================
CREATE TABLE IF NOT EXISTS ml_point_expiry_config (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
config_key VARCHAR(50) NOT NULL UNIQUE,
config_value JSONB NOT NULL,
description TEXT,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 初始化配置
INSERT INTO ml_point_expiry_config (config_key, config_value, description) VALUES
('expiry_period', '{"days": 365}', '积分有效期NULL或0表示永不过期'),
('notification_days', '{"days": [7, 3, 1]}', '过期前多少天发送提醒'),
('enabled', '{"value": true}', '是否启用积分过期功能')
ON CONFLICT (config_key) DO NOTHING;
COMMENT ON TABLE ml_point_expiry_config IS '积分过期配置表';
-- =====================================================
-- 四、创建定时任务函数
-- =====================================================
-- 1. 为新获取的积分设置过期时间
CREATE OR REPLACE FUNCTION set_points_expiry()
RETURNS void AS $$
DECLARE
expiry_days INT;
enabled BOOLEAN;
BEGIN
-- 检查是否启用过期功能
SELECT (config_value->>'value')::boolean INTO enabled
FROM ml_point_expiry_config
WHERE config_key = 'enabled';
IF NOT enabled THEN
RETURN;
END IF;
-- 获取过期天数
SELECT (config_value->>'days')::int INTO expiry_days
FROM ml_point_expiry_config
WHERE config_key = 'expiry_period';
-- 为未设置过期时间且未过期的积分记录设置过期时间
-- 只处理正积分(获取的积分)
UPDATE ml_point_records
SET expires_at = created_at + (expiry_days || ' days')::interval
WHERE expires_at IS NULL
AND is_expired = FALSE
AND points > 0;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 2. 处理过期积分
CREATE OR REPLACE FUNCTION process_expired_points()
RETURNS void AS $$
DECLARE
expired_record RECORD;
user_points_rec RECORD;
BEGIN
-- 查找所有已过期但未标记的积分记录
FOR expired_record IN
SELECT id, user_id, points, expires_at
FROM ml_point_records
WHERE expires_at IS NOT NULL
AND expires_at < NOW()
AND is_expired = FALSE
AND points > 0
LOOP
-- 标记为已过期
UPDATE ml_point_records
SET is_expired = TRUE, expired_at = NOW()
WHERE id = expired_record.id;
-- 扣减用户积分
UPDATE ml_user_points
SET points = GREATEST(0, points - expired_record.points),
updated_at = NOW()
WHERE user_id = expired_record.user_id;
-- 记录过期扣减
INSERT INTO ml_point_records (user_id, points, type, description, created_at)
VALUES (
expired_record.user_id,
-expired_record.points,
'expire',
'积分过期自动扣除',
NOW()
);
END LOOP;
-- 更新用户即将过期积分统计
FOR user_points_rec IN SELECT user_id FROM ml_user_points LOOP
UPDATE ml_user_points up
SET expiring_points = (
SELECT COALESCE(SUM(points), 0)
FROM ml_point_records
WHERE user_id = up.user_id
AND points > 0
AND is_expired = FALSE
AND expires_at IS NOT NULL
AND expires_at < NOW() + INTERVAL '30 days'
),
expiring_date = (
SELECT MIN(expires_at)::date
FROM ml_point_records
WHERE user_id = up.user_id
AND points > 0
AND is_expired = FALSE
AND expires_at IS NOT NULL
AND expires_at < NOW() + INTERVAL '30 days'
)
WHERE up.user_id = user_points_rec.user_id;
END LOOP;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 3. 生成过期提醒通知
CREATE OR REPLACE FUNCTION generate_expiry_notifications()
RETURNS void AS $$
DECLARE
notification_days INT[];
days_before INT;
expiring_record RECORD;
BEGIN
-- 获取提醒天数配置
SELECT ARRAY(SELECT jsonb_array_elements_text(config_value->'days')::int)
INTO notification_days
FROM ml_point_expiry_config
WHERE config_key = 'notification_days';
-- 遍历每个提醒天数
FOREACH days_before IN ARRAY notification_days LOOP
-- 查找即将过期的积分
FOR expiring_record IN
SELECT
user_id,
SUM(points) as total_points,
MIN(expires_at)::date as expiry_date
FROM ml_point_records
WHERE points > 0
AND is_expired = FALSE
AND expires_at IS NOT NULL
AND expires_at::date = (CURRENT_DATE + days_before)
GROUP BY user_id
LOOP
-- 检查是否已发送过该类型通知
IF NOT EXISTS (
SELECT 1 FROM ml_point_expiry_notifications
WHERE user_id = expiring_record.user_id
AND expiry_date = expiring_record.expiry_date
AND notification_type = days_before || 'days'
) THEN
-- 插入通知记录
INSERT INTO ml_point_expiry_notifications
(user_id, points_expiring, expiry_date, notification_type)
VALUES (
expiring_record.user_id,
expiring_record.total_points,
expiring_record.expiry_date,
days_before || 'days'
);
END IF;
END LOOP;
END LOOP;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 4. 获取用户即将过期积分(供前端调用)
CREATE OR REPLACE FUNCTION get_user_expiring_points(p_user_id UUID)
RETURNS TABLE(
expiring_points INT,
expiring_date DATE,
expiring_details JSONB
) AS $$
BEGIN
RETURN QUERY
SELECT
COALESCE(SUM(pr.points), 0)::INT as expiring_points,
MIN(pr.expires_at)::date as expiring_date,
jsonb_agg(
jsonb_build_object(
'points', pr.points,
'expires_at', pr.expires_at,
'description', pr.description,
'created_at', pr.created_at
)
) as expiring_details
FROM ml_point_records pr
WHERE pr.user_id = p_user_id
AND pr.points > 0
AND pr.is_expired = FALSE
AND pr.expires_at IS NOT NULL
AND pr.expires_at < NOW() + INTERVAL '30 days';
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 5. 综合定时任务(每天执行一次)
CREATE OR REPLACE FUNCTION daily_points_maintenance()
RETURNS void AS $$
BEGIN
-- 设置新积分的过期时间
PERFORM set_points_expiry();
-- 处理已过期积分
PERFORM process_expired_points();
-- 生成过期提醒
PERFORM generate_expiry_notifications();
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =====================================================
-- 五、配置 pg_cron 定时任务(如果可用)
-- =====================================================
-- 注意:需要先在 Supabase 中启用 pg_cron 扩展
-- 可以在 Supabase Dashboard -> Database -> Extensions 中启用
-- 每天凌晨2点执行积分维护任务
-- SELECT cron.schedule(
-- 'daily-points-maintenance',
-- '0 2 * * *',
-- 'SELECT daily_points_maintenance()'
-- );
-- 每小时检查并发送过期提醒(可选)
-- SELECT cron.schedule(
-- 'hourly-expiry-notification-check',
-- '0 * * * *',
-- 'SELECT generate_expiry_notifications()'
-- );
-- =====================================================
-- 六、RLS 策略
-- =====================================================
ALTER TABLE ml_point_expiry_notifications ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own expiry notifications"
ON ml_point_expiry_notifications FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
-- =====================================================
-- 七、创建视图方便查询
-- =====================================================
-- 用户积分概览视图
CREATE OR REPLACE VIEW ml_user_points_overview AS
SELECT
up.user_id,
up.points as current_points,
up.total_earned,
COALESCE(up.expiring_points, 0) as expiring_points,
up.expiring_date,
(SELECT COUNT(*) FROM ml_signin_records WHERE user_id = up.user_id) as total_signin_days,
(SELECT MAX(continuous_days) FROM ml_signin_records WHERE user_id = up.user_id) as max_continuous_days
FROM ml_user_points up;
-- =====================================================
-- 八、测试数据(可选)
-- =====================================================
-- 插入一些即将过期的测试积分记录
-- INSERT INTO ml_point_records (user_id, points, type, description, expires_at, created_at)
-- VALUES
-- ('YOUR_USER_ID', 50, 'signin', '测试即将过期积分', NOW() + INTERVAL '5 days', NOW() - INTERVAL '360 days'),
-- ('YOUR_USER_ID', 30, 'shopping', '测试购物积分', NOW() + INTERVAL '10 days', NOW() - INTERVAL '355 days');
-- =====================================================
-- 九、使用说明
-- =====================================================
/*
使用方法:
1. 手动执行定时任务:
SELECT daily_points_maintenance();
2. 查询用户即将过期积分:
SELECT * FROM get_user_expiring_points('用户ID');
3. 修改积分有效期配置:
UPDATE ml_point_expiry_config
SET config_value = '{"days": 180}', updated_at = NOW()
WHERE config_key = 'expiry_period';
4. 禁用积分过期功能:
UPDATE ml_point_expiry_config
SET config_value = '{"value": false}', updated_at = NOW()
WHERE config_key = 'enabled';
5. 修改提醒天数:
UPDATE ml_point_expiry_config
SET config_value = '{"days": [14, 7, 3, 1]}', updated_at = NOW()
WHERE config_key = 'notification_days';
注意:
- 如果 Supabase 项目启用了 pg_cron可以取消注释定时任务配置
- 如果没有 pg_cron需要通过外部定时任务如 Supabase Edge Functions调用 daily_points_maintenance()
*/

View File

@@ -0,0 +1,570 @@
-- =====================================================
-- 推销模式功能 - 数据库脚本(第一阶段)
-- 包含:用户余额系统、分享免单系统、会员等级系统
-- 创建日期: 2026-03-06
-- =====================================================
-- =====================================================
-- 一、用户余额系统
-- =====================================================
-- 1. 用户余额表
CREATE TABLE IF NOT EXISTS ml_user_balance (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
balance DECIMAL(10,2) DEFAULT 0,
frozen_balance DECIMAL(10,2) DEFAULT 0,
total_earned DECIMAL(10,2) DEFAULT 0,
total_withdrawn DECIMAL(10,2) DEFAULT 0,
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id)
);
CREATE INDEX IF NOT EXISTS idx_user_balance_user_id ON ml_user_balance(user_id);
COMMENT ON TABLE ml_user_balance IS '用户余额表';
COMMENT ON COLUMN ml_user_balance.balance IS '可用余额';
COMMENT ON COLUMN ml_user_balance.frozen_balance IS '冻结余额';
COMMENT ON COLUMN ml_user_balance.total_earned IS '累计获得';
COMMENT ON COLUMN ml_user_balance.total_withdrawn IS '累计提现';
-- 2. 余额变动记录表
CREATE TABLE IF NOT EXISTS ml_balance_records (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
type VARCHAR(50) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
balance_before DECIMAL(10,2) NOT NULL DEFAULT 0,
balance_after DECIMAL(10,2) NOT NULL DEFAULT 0,
related_id UUID,
description VARCHAR(200),
operator_id UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_balance_records_user_id ON ml_balance_records(user_id);
CREATE INDEX IF NOT EXISTS idx_balance_records_type ON ml_balance_records(type);
CREATE INDEX IF NOT EXISTS idx_balance_records_created_at ON ml_balance_records(created_at);
COMMENT ON TABLE ml_balance_records IS '余额变动记录表';
COMMENT ON COLUMN ml_balance_records.type IS '类型free_order-免单奖励rebate-返利withdraw-提现clear-清零manual-手动调整';
-- =====================================================
-- 二、分享免单系统
-- =====================================================
-- 1. 分享记录表
CREATE TABLE IF NOT EXISTS ml_share_records (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
product_id UUID NOT NULL REFERENCES ml_products(id),
order_id UUID NOT NULL REFERENCES ml_orders(id),
order_item_id UUID,
share_code VARCHAR(20) NOT NULL UNIQUE,
product_name VARCHAR(200),
product_image VARCHAR(500),
product_price DECIMAL(10,2),
required_count INT DEFAULT 4,
current_count INT DEFAULT 0,
status INT DEFAULT 0,
reward_amount DECIMAL(10,2),
created_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ,
expired_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_share_records_user_id ON ml_share_records(user_id);
CREATE INDEX IF NOT EXISTS idx_share_records_share_code ON ml_share_records(share_code);
CREATE INDEX IF NOT EXISTS idx_share_records_status ON ml_share_records(status);
COMMENT ON TABLE ml_share_records IS '分享免单记录表';
COMMENT ON COLUMN ml_share_records.status IS '状态0-进行中1-已完成2-已失效3-已过期';
-- 2. 二级购买记录表
CREATE TABLE IF NOT EXISTS ml_secondary_purchases (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
share_record_id UUID NOT NULL REFERENCES ml_share_records(id) ON DELETE CASCADE,
buyer_id UUID NOT NULL REFERENCES auth.users(id),
order_id UUID NOT NULL REFERENCES ml_orders(id),
quantity INT DEFAULT 1,
unit_price DECIMAL(10,2),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(order_id, share_record_id)
);
CREATE INDEX IF NOT EXISTS idx_secondary_purchases_share_record_id ON ml_secondary_purchases(share_record_id);
CREATE INDEX IF NOT EXISTS idx_secondary_purchases_buyer_id ON ml_secondary_purchases(buyer_id);
COMMENT ON TABLE ml_secondary_purchases IS '二级用户购买记录表';
-- 3. 免单奖励记录表
CREATE TABLE IF NOT EXISTS ml_free_order_rewards (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
share_record_id UUID NOT NULL REFERENCES ml_share_records(id),
amount DECIMAL(10,2) NOT NULL,
status INT DEFAULT 0,
balance_record_id UUID REFERENCES ml_balance_records(id),
cleared_at TIMESTAMPTZ,
cleared_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_free_order_rewards_user_id ON ml_free_order_rewards(user_id);
CREATE INDEX IF NOT EXISTS idx_free_order_rewards_status ON ml_free_order_rewards(status);
COMMENT ON TABLE ml_free_order_rewards IS '免单奖励记录表';
COMMENT ON COLUMN ml_free_order_rewards.status IS '状态0-待发放1-已发放2-已清零';
-- =====================================================
-- 三、会员等级系统
-- =====================================================
-- 1. 会员等级配置表
CREATE TABLE IF NOT EXISTS ml_member_levels (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
min_amount DECIMAL(10,2) DEFAULT 0,
discount DECIMAL(5,4) DEFAULT 1.0000,
icon VARCHAR(200),
description TEXT,
sort_order INT DEFAULT 0,
status INT DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE ml_member_levels IS '会员等级配置表';
COMMENT ON COLUMN ml_member_levels.discount IS '折扣率0.85表示85折';
-- 初始化会员等级数据
INSERT INTO ml_member_levels (id, name, min_amount, discount, description, sort_order) VALUES
(0, '普通会员', 0, 1.0000, '注册即可成为普通会员', 0),
(1, '铜牌会员', 500, 0.9800, '累计消费500元升级', 1),
(2, '银牌会员', 2000, 0.9500, '累计消费2000元升级', 2),
(3, '金牌会员', 5000, 0.9200, '累计消费5000元升级', 3),
(4, '钻石会员', 10000, 0.8800, '累计消费10000元升级', 4),
(5, 'VIP会员', 0, 0.8500, '商家特邀会员', 5)
ON CONFLICT (id) DO NOTHING;
-- 2. 用户会员信息扩展字段(添加到 ml_user_profiles
ALTER TABLE ml_user_profiles ADD COLUMN IF NOT EXISTS member_level INT DEFAULT 0;
ALTER TABLE ml_user_profiles ADD COLUMN IF NOT EXISTS total_spent DECIMAL(10,2) DEFAULT 0;
ALTER TABLE ml_user_profiles ADD COLUMN IF NOT EXISTS level_updated_at TIMESTAMPTZ;
ALTER TABLE ml_user_profiles ADD COLUMN IF NOT EXISTS manual_level BOOLEAN DEFAULT FALSE;
ALTER TABLE ml_user_profiles ADD COLUMN IF NOT EXISTS manual_level_by UUID;
ALTER TABLE ml_user_profiles ADD COLUMN IF NOT EXISTS manual_level_at TIMESTAMPTZ;
COMMENT ON COLUMN ml_user_profiles.member_level IS '当前会员等级';
COMMENT ON COLUMN ml_user_profiles.total_spent IS '累计消费金额';
COMMENT ON COLUMN ml_user_profiles.manual_level IS '是否手动设置等级';
-- 3. 会员等级变更记录表
CREATE TABLE IF NOT EXISTS ml_member_level_logs (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id),
old_level INT DEFAULT 0,
new_level INT NOT NULL,
reason VARCHAR(200),
operator_id UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_member_level_logs_user_id ON ml_member_level_logs(user_id);
COMMENT ON TABLE ml_member_level_logs IS '会员等级变更记录表';
-- =====================================================
-- 四、RLS 策略
-- =====================================================
-- 用户余额表 RLS
ALTER TABLE ml_user_balance ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own balance"
ON ml_user_balance FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own balance"
ON ml_user_balance FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own balance"
ON ml_user_balance FOR UPDATE
TO authenticated
USING (auth.uid() = user_id);
-- 余额记录表 RLS
ALTER TABLE ml_balance_records ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own balance records"
ON ml_balance_records FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own balance records"
ON ml_balance_records FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
-- 分享记录表 RLS
ALTER TABLE ml_share_records ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own share records"
ON ml_share_records FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own share records"
ON ml_share_records FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own share records"
ON ml_share_records FOR UPDATE
TO authenticated
USING (auth.uid() = user_id);
-- 允许通过分享码查询(用于验证分享码)
CREATE POLICY "Anyone can view by share code"
ON ml_share_records FOR SELECT
TO authenticated, anon
USING (share_code IS NOT NULL);
-- 二级购买记录表 RLS
ALTER TABLE ml_secondary_purchases ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own secondary purchases"
ON ml_secondary_purchases FOR SELECT
TO authenticated
USING (buyer_id = auth.uid() OR EXISTS (
SELECT 1 FROM ml_share_records WHERE id = share_record_id AND user_id = auth.uid()
));
CREATE POLICY "Users can insert secondary purchases"
ON ml_secondary_purchases FOR INSERT
TO authenticated
WITH CHECK (true);
-- 免单奖励记录表 RLS
ALTER TABLE ml_free_order_rewards ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own rewards"
ON ml_free_order_rewards FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
-- 会员等级配置表 RLS所有人可查看
ALTER TABLE ml_member_levels ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view member levels"
ON ml_member_levels FOR SELECT
TO authenticated, anon
USING (status = 1);
-- 会员等级变更记录表 RLS
ALTER TABLE ml_member_level_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own level logs"
ON ml_member_level_logs FOR SELECT
TO authenticated
USING (auth.uid() = user_id);
-- =====================================================
-- 五、数据库函数
-- =====================================================
-- 1. 生成分享码函数
CREATE OR REPLACE FUNCTION generate_share_code()
RETURNS VARCHAR(20) AS $$
DECLARE
chars TEXT := 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
result VARCHAR(20) := '';
i INT;
BEGIN
FOR i IN 1..8 LOOP
result := result || substr(chars, floor(random() * length(chars) + 1)::int, 1);
END LOOP;
RETURN result;
END;
$$ LANGUAGE plpgsql;
-- 2. 增加余额函数
CREATE OR REPLACE FUNCTION add_user_balance(
p_user_id UUID,
p_amount DECIMAL(10,2),
p_type VARCHAR(50),
p_related_id UUID DEFAULT NULL,
p_description VARCHAR(200) DEFAULT NULL,
p_operator_id UUID DEFAULT NULL
)
RETURNS BOOLEAN AS $$
DECLARE
v_balance_before DECIMAL(10,2);
v_balance_after DECIMAL(10,2);
BEGIN
-- 获取当前余额
SELECT COALESCE(balance, 0) INTO v_balance_before
FROM ml_user_balance
WHERE user_id = p_user_id;
IF v_balance_before IS NULL THEN
-- 创建余额记录
INSERT INTO ml_user_balance (user_id, balance, total_earned)
VALUES (p_user_id, p_amount, p_amount);
v_balance_before := 0;
v_balance_after := p_amount;
ELSE
-- 更新余额
UPDATE ml_user_balance
SET balance = balance + p_amount,
total_earned = total_earned + CASE WHEN p_amount > 0 THEN p_amount ELSE 0 END,
updated_at = NOW()
WHERE user_id = p_user_id;
v_balance_after := v_balance_before + p_amount;
END IF;
-- 记录变动
INSERT INTO ml_balance_records (user_id, type, amount, balance_before, balance_after, related_id, description, operator_id)
VALUES (p_user_id, p_type, p_amount, v_balance_before, v_balance_after, p_related_id, p_description, p_operator_id);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 3. 清零用户余额函数
CREATE OR REPLACE FUNCTION clear_user_balance(
p_user_id UUID,
p_operator_id UUID,
p_description VARCHAR(200) DEFAULT '商家清零余额'
)
RETURNS BOOLEAN AS $$
DECLARE
v_balance_before DECIMAL(10,2);
BEGIN
-- 获取当前余额
SELECT balance INTO v_balance_before
FROM ml_user_balance
WHERE user_id = p_user_id;
IF v_balance_before IS NULL OR v_balance_before = 0 THEN
RETURN FALSE;
END IF;
-- 清零余额
UPDATE ml_user_balance
SET balance = 0,
updated_at = NOW()
WHERE user_id = p_user_id;
-- 记录变动
INSERT INTO ml_balance_records (user_id, type, amount, balance_before, balance_after, description, operator_id)
VALUES (p_user_id, 'clear', -v_balance_before, v_balance_before, 0, p_description, p_operator_id);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 4. 检查并升级会员等级函数
CREATE OR REPLACE FUNCTION check_and_upgrade_member_level(p_user_id UUID)
RETURNS INT AS $$
DECLARE
v_total_spent DECIMAL(10,2);
v_current_level INT;
v_new_level INT;
v_manual_level BOOLEAN;
BEGIN
-- 获取用户当前信息
SELECT member_level, total_spent, manual_level
INTO v_current_level, v_total_spent, v_manual_level
FROM ml_user_profiles
WHERE user_id = p_user_id;
-- 如果是手动设置的等级,不自动升级
IF v_manual_level = TRUE THEN
RETURN v_current_level;
END IF;
-- 根据消费金额计算新等级
SELECT id INTO v_new_level
FROM ml_member_levels
WHERE status = 1 AND min_amount <= COALESCE(v_total_spent, 0)
ORDER BY min_amount DESC
LIMIT 1;
IF v_new_level IS NULL THEN
v_new_level := 0;
END IF;
-- 如果等级有变化,更新并记录
IF v_new_level > COALESCE(v_current_level, 0) THEN
UPDATE ml_user_profiles
SET member_level = v_new_level,
level_updated_at = NOW()
WHERE user_id = p_user_id;
INSERT INTO ml_member_level_logs (user_id, old_level, new_level, reason)
VALUES (p_user_id, COALESCE(v_current_level, 0), v_new_level, '累计消费自动升级');
RETURN v_new_level;
END IF;
RETURN COALESCE(v_current_level, 0);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 5. 处理分享购买函数
CREATE OR REPLACE FUNCTION process_share_purchase(
p_share_code VARCHAR(20),
p_buyer_id UUID,
p_order_id UUID,
p_quantity INT,
p_unit_price DECIMAL(10,2)
)
RETURNS BOOLEAN AS $$
DECLARE
v_share_record ml_share_records%ROWTYPE;
v_required_count INT;
v_reward_amount DECIMAL(10,2);
BEGIN
-- 查找分享记录
SELECT * INTO v_share_record
FROM ml_share_records
WHERE share_code = p_share_code AND status = 0;
IF NOT FOUND THEN
RETURN FALSE;
END IF;
-- 不能购买自己分享的商品
IF v_share_record.user_id = p_buyer_id THEN
RETURN FALSE;
END IF;
-- 检查是否已购买过(同一用户对同一分享记录只算一次)
IF EXISTS (SELECT 1 FROM ml_secondary_purchases WHERE share_record_id = v_share_record.id AND buyer_id = p_buyer_id) THEN
RETURN FALSE;
END IF;
-- 记录购买
INSERT INTO ml_secondary_purchases (share_record_id, buyer_id, order_id, quantity, unit_price)
VALUES (v_share_record.id, p_buyer_id, p_order_id, p_quantity, p_unit_price);
-- 更新计数
UPDATE ml_share_records
SET current_count = current_count + 1
WHERE id = v_share_record.id;
-- 检查是否达标
SELECT current_count, required_count INTO v_required_count, v_required_count
FROM ml_share_records WHERE id = v_share_record.id;
IF v_share_record.current_count + 1 >= v_share_record.required_count THEN
-- 计算奖励金额
v_reward_amount := v_share_record.product_price;
-- 更新分享记录状态
UPDATE ml_share_records
SET status = 1, completed_at = NOW(), reward_amount = v_reward_amount
WHERE id = v_share_record.id;
-- 创建奖励记录
INSERT INTO ml_free_order_rewards (user_id, share_record_id, amount)
VALUES (v_share_record.user_id, v_share_record.id, v_reward_amount);
-- 增加用户余额
PERFORM add_user_balance(
v_share_record.user_id,
v_reward_amount,
'free_order',
v_share_record.id,
'分享免单奖励:' || v_share_record.product_name
);
-- 更新奖励记录状态
UPDATE ml_free_order_rewards
SET status = 1
WHERE share_record_id = v_share_record.id;
END IF;
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- =====================================================
-- 六、触发器
-- =====================================================
-- 订单完成后更新用户累计消费并检查会员等级
CREATE OR REPLACE FUNCTION update_user_total_spent()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.status = 4 AND (OLD.status != 4 OR OLD.status IS NULL) THEN
-- 更新累计消费
UPDATE ml_user_profiles
SET total_spent = COALESCE(total_spent, 0) + NEW.total_amount
WHERE user_id = NEW.user_id;
-- 检查会员等级
PERFORM check_and_upgrade_member_level(NEW.user_id);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 创建触发器(如果不存在)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger WHERE tgname = 'trigger_update_user_total_spent'
) THEN
CREATE TRIGGER trigger_update_user_total_spent
AFTER UPDATE ON ml_orders
FOR EACH ROW
EXECUTE FUNCTION update_user_total_spent();
END IF;
END $$;
-- =====================================================
-- 七、视图
-- =====================================================
-- 用户余额概览视图
CREATE OR REPLACE VIEW ml_user_balance_overview AS
SELECT
ub.user_id,
ub.balance,
ub.frozen_balance,
ub.total_earned,
ub.total_withdrawn,
up.member_level,
ml.name as member_level_name,
ml.discount as member_discount,
up.total_spent
FROM ml_user_balance ub
LEFT JOIN ml_user_profiles up ON ub.user_id = up.user_id
LEFT JOIN ml_member_levels ml ON up.member_level = ml.id;
-- 分享进度视图
CREATE OR REPLACE VIEW ml_share_progress_view AS
SELECT
sr.id,
sr.user_id,
sr.product_id,
sr.product_name,
sr.product_image,
sr.product_price,
sr.share_code,
sr.required_count,
sr.current_count,
sr.status,
sr.reward_amount,
sr.created_at,
sr.completed_at,
sr.required_count - sr.current_count as remaining_count
FROM ml_share_records sr;