Files
medical-mall/doc_mall/consumer/sql/points_expiry_functions.sql
2026-03-06 17:30:50 +08:00

357 lines
12 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =====================================================
-- 积分过期提醒功能 - 数据库脚本
-- 创建日期: 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()
*/