feat: database-first refactor, unified architecture and visual developer manual
Summary of changes: - Consolidated Studio+CMS and Experience+LMS into unified services. - Moved core business logic (enrollment, grading, auth) to PostgreSQL functions. - Implemented advanced auditing via DB triggers and session context. - Added gamification (XP/Levels/Leaderboards) and logic encapsulation. - Updated installation/diagnostic scripts for the new architecture. - Created a comprehensive Visual Developer Manual in README.md with hardware scaling.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
-- Migration: LMS Logic Functions (XP and Enrollment)
|
||||
|
||||
-- 1. Function to award XP
|
||||
CREATE OR REPLACE FUNCTION fn_award_xp(
|
||||
p_user_id UUID,
|
||||
p_org_id UUID,
|
||||
p_amount INTEGER,
|
||||
p_reason TEXT,
|
||||
p_entity_type TEXT DEFAULT NULL,
|
||||
p_entity_id UUID DEFAULT NULL
|
||||
) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
-- Update XP in users table
|
||||
UPDATE users
|
||||
SET xp = xp + p_amount,
|
||||
level = FLOOR(SQRT((xp + p_amount)::FLOAT / 100)) + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = p_user_id;
|
||||
|
||||
-- Log to points_log
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'points_log') THEN
|
||||
INSERT INTO points_log (user_id, organization_id, amount, reason, entity_type, entity_id)
|
||||
VALUES (p_user_id, p_org_id, p_amount, p_reason, p_entity_type, p_entity_id);
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 2. Function for Course Cohort Analytics
|
||||
CREATE OR REPLACE FUNCTION fn_get_cohort_analytics(p_course_id UUID)
|
||||
RETURNS TABLE (
|
||||
period TEXT,
|
||||
student_count BIGINT,
|
||||
completion_rate FLOAT4
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH cohort_students AS (
|
||||
SELECT
|
||||
user_id,
|
||||
TO_CHAR(enrolled_at, 'YYYY-MM') as v_period
|
||||
FROM enrollments
|
||||
WHERE course_id = p_course_id
|
||||
),
|
||||
course_lesson_count AS (
|
||||
SELECT COUNT(*)::float4 as total_lessons
|
||||
FROM lessons
|
||||
WHERE module_id IN (SELECT id FROM modules WHERE course_id = p_course_id)
|
||||
)
|
||||
SELECT
|
||||
cs.v_period as period,
|
||||
COUNT(DISTINCT cs.user_id) as student_count,
|
||||
COALESCE(AVG(
|
||||
(SELECT COUNT(DISTINCT lesson_id)::float4 FROM user_grades WHERE user_id = cs.user_id AND course_id = p_course_id) /
|
||||
NULLIF((SELECT total_lessons FROM course_lesson_count), 0)
|
||||
), 0)::float4 as completion_rate
|
||||
FROM cohort_students cs
|
||||
GROUP BY cs.v_period
|
||||
ORDER BY cs.v_period DESC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 3. Retention Data Function
|
||||
CREATE OR REPLACE FUNCTION fn_get_retention_data(p_course_id UUID)
|
||||
RETURNS TABLE (
|
||||
lesson_id UUID,
|
||||
lesson_title VARCHAR,
|
||||
student_count BIGINT
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
l.id as lesson_id,
|
||||
l.title as lesson_title,
|
||||
COUNT(DISTINCT ug.user_id) as student_count
|
||||
FROM lessons l
|
||||
LEFT JOIN user_grades ug ON l.id = ug.lesson_id
|
||||
WHERE l.module_id IN (SELECT id FROM modules WHERE course_id = p_course_id)
|
||||
GROUP BY l.id, l.title, l.position
|
||||
ORDER BY l.position;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 4. Enrollment Function
|
||||
CREATE OR REPLACE FUNCTION fn_enroll_student(
|
||||
p_organization_id UUID,
|
||||
p_user_id UUID,
|
||||
p_course_id UUID
|
||||
) RETURNS SETOF enrollments AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
INSERT INTO enrollments (organization_id, user_id, course_id)
|
||||
VALUES (p_organization_id, p_user_id, p_course_id)
|
||||
ON CONFLICT (user_id, course_id) DO UPDATE SET enrolled_at = NOW()
|
||||
RETURNING *;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 5. Grading Function (Upsert) with Automated Logic
|
||||
CREATE OR REPLACE FUNCTION fn_upsert_user_grade(
|
||||
p_organization_id UUID,
|
||||
p_user_id UUID,
|
||||
p_course_id UUID,
|
||||
p_lesson_id UUID,
|
||||
p_score FLOAT4,
|
||||
p_metadata JSONB DEFAULT NULL
|
||||
) RETURNS SETOF user_grades AS $$
|
||||
DECLARE
|
||||
v_grade user_grades;
|
||||
v_xp_amount INTEGER := 20; -- Default XP for completion
|
||||
v_badge_id UUID;
|
||||
BEGIN
|
||||
-- 1. Upsert grade
|
||||
INSERT INTO user_grades (organization_id, user_id, course_id, lesson_id, score, metadata, attempts_count)
|
||||
VALUES (p_organization_id, p_user_id, p_course_id, p_lesson_id, p_score, p_metadata, 1)
|
||||
ON CONFLICT (user_id, lesson_id) DO UPDATE SET
|
||||
score = EXCLUDED.score,
|
||||
metadata = EXCLUDED.metadata,
|
||||
attempts_count = user_grades.attempts_count + 1,
|
||||
updated_at = NOW()
|
||||
RETURNING * INTO v_grade;
|
||||
|
||||
-- 2. Award XP automatically
|
||||
PERFORM fn_award_xp(p_user_id, p_organization_id, v_xp_amount, 'lesson_completion', 'lesson', p_lesson_id);
|
||||
|
||||
-- 3. Check for new badges
|
||||
FOR v_badge_id IN
|
||||
SELECT id FROM badges
|
||||
WHERE organization_id = p_organization_id
|
||||
AND requirement_type = 'points'
|
||||
AND requirement_value <= (SELECT xp FROM users WHERE id = p_user_id)
|
||||
AND id NOT IN (SELECT badge_id FROM user_badges WHERE user_id = p_user_id)
|
||||
LOOP
|
||||
INSERT INTO user_badges (user_id, badge_id) VALUES (p_user_id, v_badge_id) ON CONFLICT DO NOTHING;
|
||||
END LOOP;
|
||||
|
||||
RETURN NEXT v_grade;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Migration: Create Webhooks Table for LMS
|
||||
CREATE TABLE webhooks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
url VARCHAR(500) NOT NULL,
|
||||
events VARCHAR(50)[] NOT NULL, -- e.g., ['user.enrolled', 'lesson.completed', 'course.completed']
|
||||
secret VARCHAR(255), -- For HMAC-SHA256 signatures
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Index for organization_id
|
||||
CREATE INDEX idx_webhooks_organization_id ON webhooks(organization_id);
|
||||
Reference in New Issue
Block a user