-- 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 事件';