-- ===================================================== -- 积分过期提醒功能 - 数据库脚本 -- 创建日期: 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() */