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