143 lines
6.1 KiB
SQL
143 lines
6.1 KiB
SQL
-- Training stream events table & policies for Supabase realtime integration
|
||
-- Generated by GitHub Copilot to support the体育课训练 dashboard mock-to-cloud transition.
|
||
|
||
CREATE TABLE IF NOT EXISTS public.training_stream_events (
|
||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 记录ID
|
||
training_id uuid NOT NULL, -- 训练会话ID,未来可关联 training_sessions
|
||
event_type VARCHAR(32) NOT NULL, -- 事件类型:ack / telemetry / state / summary 等
|
||
class_id uuid NOT NULL REFERENCES public.ak_classes(id) ON DELETE CASCADE, -- 班级ID
|
||
student_id uuid REFERENCES public.ak_users(id) ON DELETE CASCADE, -- 学生ID,可为空表示班级级别事件
|
||
device_id uuid REFERENCES public.ak_devices(id) ON DELETE SET NULL, -- 设备ID
|
||
status VARCHAR(64), -- 状态描述,如 online/offline/ack_failed
|
||
ack BOOLEAN, -- 是否为应答成功
|
||
metrics JSONB, -- 指标明细:hr/spo2/temp 等
|
||
payload JSONB, -- 额外原始载荷
|
||
ingest_source VARCHAR(32) DEFAULT 'gateway', -- 来源:gateway/mock/simulator 等
|
||
ingest_note TEXT, -- 额外说明,如模拟批次
|
||
recorded_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), -- 事件发生时间
|
||
ingested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), -- 写入时间
|
||
CONSTRAINT training_stream_events_target_chk CHECK (
|
||
class_id IS NOT NULL
|
||
)
|
||
);
|
||
|
||
COMMENT ON TABLE public.training_stream_events IS '课堂训练实时事件流,供 Supabase realtime 推送给教师端';
|
||
COMMENT ON COLUMN public.training_stream_events.training_id IS '训练会话ID(同一课堂 session 内共享)';
|
||
COMMENT ON COLUMN public.training_stream_events.event_type IS '事件类型:ack/telemetry/state/summary 等';
|
||
COMMENT ON COLUMN public.training_stream_events.class_id IS '班级ID,限定可见范围';
|
||
COMMENT ON COLUMN public.training_stream_events.student_id IS '学生ID;为空表示班级级别事件';
|
||
COMMENT ON COLUMN public.training_stream_events.device_id IS '关联设备;为空表示未绑定或无关设备事件';
|
||
COMMENT ON COLUMN public.training_stream_events.metrics IS '结构化指标(JSON),用于实时展示';
|
||
COMMENT ON COLUMN public.training_stream_events.payload IS '原始负载(JSON),用于调试或回放';
|
||
COMMENT ON COLUMN public.training_stream_events.ingest_source IS '事件来源:gateway/mock/simulator/service 等';
|
||
COMMENT ON COLUMN public.training_stream_events.ingest_note IS '模拟批次、异常说明等';
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_training_stream_events_training_recorded
|
||
ON public.training_stream_events (training_id, recorded_at DESC);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_training_stream_events_class_recorded
|
||
ON public.training_stream_events (class_id, recorded_at DESC);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_training_stream_events_student_recorded
|
||
ON public.training_stream_events (student_id, recorded_at DESC);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_training_stream_events_event_type
|
||
ON public.training_stream_events (event_type);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_training_stream_events_device_recorded
|
||
ON public.training_stream_events (device_id, recorded_at DESC);
|
||
|
||
ALTER TABLE public.training_stream_events ENABLE ROW LEVEL SECURITY;
|
||
|
||
-- 服务端(service_role)拥有完全访问权限,用于 Edge Function / Gateway 写入
|
||
CREATE POLICY "service role full access" ON public.training_stream_events
|
||
FOR ALL
|
||
USING (auth.role() = 'service_role')
|
||
WITH CHECK (auth.role() = 'service_role');
|
||
|
||
-- 教师可以读取自己负责班级的训练事件
|
||
CREATE POLICY "teacher read class training events" ON public.training_stream_events
|
||
FOR SELECT
|
||
TO authenticated
|
||
USING (
|
||
auth.role() = 'service_role' OR
|
||
public.current_user_has_permission('admin.system.manage') OR
|
||
public.current_user_has_permission('school_admin.class.manage') OR
|
||
EXISTS (
|
||
SELECT 1
|
||
FROM public.ak_users u
|
||
JOIN public.ak_teacher_roles tr ON tr.user_id = u.id
|
||
WHERE u.auth_id = auth.uid()
|
||
AND tr.class_id = public.training_stream_events.class_id
|
||
)
|
||
);
|
||
|
||
-- 学生可查看自己的训练事件
|
||
CREATE POLICY "student read own training events" ON public.training_stream_events
|
||
FOR SELECT
|
||
TO authenticated
|
||
USING (
|
||
public.current_user_has_permission('student.training.read') AND
|
||
EXISTS (
|
||
SELECT 1
|
||
FROM public.ak_users u
|
||
WHERE u.auth_id = auth.uid()
|
||
AND u.id = public.training_stream_events.student_id
|
||
)
|
||
);
|
||
DROP POLICY "teacher insert training events" ON public.training_stream_events;
|
||
|
||
-- 仅允许服务端或具备班级权限的教师写入事件(方便课堂模拟工具)
|
||
CREATE POLICY "teacher insert training events" ON public.training_stream_events
|
||
FOR INSERT
|
||
TO authenticated
|
||
WITH CHECK (
|
||
auth.role() = 'service_role' OR
|
||
EXISTS (
|
||
SELECT 1
|
||
FROM public.ak_users u
|
||
JOIN public.ak_teacher_roles tr ON tr.user_id = u.id
|
||
WHERE u.auth_id = auth.uid()
|
||
AND tr.class_id = public.training_stream_events.class_id
|
||
)
|
||
);
|
||
|
||
-- 清理时只允许服务角色删除
|
||
CREATE POLICY "service role delete training events" ON public.training_stream_events
|
||
FOR DELETE
|
||
USING (auth.role() = 'service_role');
|
||
|
||
-- 将表加入 Supabase realtime publication,避免重复添加导致错误
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (
|
||
SELECT 1 FROM pg_catalog.pg_publication_tables
|
||
WHERE pubname = 'supabase_realtime'
|
||
AND schemaname = 'public'
|
||
AND tablename = 'training_stream_events'
|
||
) THEN
|
||
EXECUTE 'ALTER PUBLICATION supabase_realtime ADD TABLE public.training_stream_events';
|
||
END IF;
|
||
END;
|
||
$$;
|
||
|
||
-- 便于查询学生最近一次指标的视图
|
||
CREATE OR REPLACE VIEW public.training_stream_latest_metrics AS
|
||
SELECT DISTINCT ON (training_id, student_id)
|
||
id,
|
||
training_id,
|
||
class_id,
|
||
student_id,
|
||
device_id,
|
||
event_type,
|
||
status,
|
||
metrics,
|
||
payload,
|
||
recorded_at,
|
||
ingested_at
|
||
FROM public.training_stream_events
|
||
WHERE event_type IN ('telemetry', 'metrics')
|
||
ORDER BY training_id, student_id, recorded_at DESC, ingested_at DESC, id DESC;
|
||
|
||
COMMENT ON VIEW public.training_stream_latest_metrics IS '每个学生在当前训练中的最近一次 telemetry/metrics 事件';
|