diff --git a/doc_mall/consumer/sql/points_and_reviews_tables.sql b/doc_mall/consumer/sql/points_and_reviews_tables.sql new file mode 100644 index 00000000..6ba33686 --- /dev/null +++ b/doc_mall/consumer/sql/points_and_reviews_tables.sql @@ -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; diff --git a/doc_mall/consumer/sql/points_expiry_functions.sql b/doc_mall/consumer/sql/points_expiry_functions.sql new file mode 100644 index 00000000..605b5ca7 --- /dev/null +++ b/doc_mall/consumer/sql/points_expiry_functions.sql @@ -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() +*/ diff --git a/doc_mall/consumer/sql/promotion_system_tables.sql b/doc_mall/consumer/sql/promotion_system_tables.sql new file mode 100644 index 00000000..b3c9670d --- /dev/null +++ b/doc_mall/consumer/sql/promotion_system_tables.sql @@ -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; diff --git a/pages.json b/pages.json index 750bb491..3c298d34 100644 --- a/pages.json +++ b/pages.json @@ -258,6 +258,60 @@ "navigationBarTitleText": "积分管理" } }, + { + "path": "points/signin", + "style": { + "navigationBarTitleText": "每日签到" + } + }, + { + "path": "points/exchange", + "style": { + "navigationBarTitleText": "积分兑换" + } + }, + { + "path": "points/exchange-records", + "style": { + "navigationBarTitleText": "兑换记录" + } + }, + { + "path": "product-reviews", + "style": { + "navigationBarTitleText": "商品评价" + } + }, + { + "path": "my-reviews", + "style": { + "navigationBarTitleText": "我的评价" + } + }, + { + "path": "balance/index", + "style": { + "navigationBarTitleText": "我的余额" + } + }, + { + "path": "share/index", + "style": { + "navigationBarTitleText": "我的分享" + } + }, + { + "path": "share/detail", + "style": { + "navigationBarTitleText": "分享详情" + } + }, + { + "path": "member/index", + "style": { + "navigationBarTitleText": "会员中心" + } + }, { "path": "red-packets/index", "style": { diff --git a/pages/main/profile.uvue b/pages/main/profile.uvue index fc21dc56..e0823881 100644 --- a/pages/main/profile.uvue +++ b/pages/main/profile.uvue @@ -1,4 +1,4 @@ - +