Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
-- 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 事件';