-- ===================================================== -- 推销模式功能 - 数据库脚本(第一阶段) -- 包含:用户余额系统、分享免单系统、会员等级系统 -- 创建日期: 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;