diff --git a/apps/api/src/db/0041_lesson_mode.sql b/apps/api/src/db/0041_lesson_mode.sql new file mode 100644 index 0000000..2f07d5e --- /dev/null +++ b/apps/api/src/db/0041_lesson_mode.sql @@ -0,0 +1,2 @@ +-- 这节课做什么:写代码 / 画流程图 / 先画再写。以前布置的都是写代码;带 DEFAULT,没升级的旧代码照样能布置 +ALTER TABLE "class_lesson" ADD COLUMN "mode" text DEFAULT 'code' NOT NULL; diff --git a/apps/api/src/db/meta/0041_snapshot.json b/apps/api/src/db/meta/0041_snapshot.json new file mode 100644 index 0000000..2bef65d --- /dev/null +++ b/apps/api/src/db/meta/0041_snapshot.json @@ -0,0 +1,4002 @@ +{ + "id": "801f9473-f236-4855-a50b-229084181a4a", + "prevId": "3d44f0d2-6a01-4f02-89f0-b8befc3aa0c6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.achievement": { + "name": "achievement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "achievement_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rarity": { + "name": "rarity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operator": { + "name": "operator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "unlock_count": { + "name": "unlock_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.acm_contest_rank": { + "name": "acm_contest_rank", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "submission_number": { + "name": "submission_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "accepted_number": { + "name": "accepted_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_time": { + "name": "total_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "submission_info": { + "name": "submission_info", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "contest_id": { + "name": "contest_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "acm_rank_contest_user_idx": { + "name": "acm_rank_contest_user_idx", + "columns": [ + { + "expression": "contest_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "acm_rank_order_idx": { + "name": "acm_rank_order_idx", + "columns": [ + { + "expression": "contest_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + }, + { + "expression": "accepted_number", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + }, + { + "expression": "total_time", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "acm_contest_rank_contest_id_21030ccd_fk_contest_id": { + "name": "acm_contest_rank_contest_id_21030ccd_fk_contest_id", + "tableFrom": "acm_contest_rank", + "tableTo": "contest", + "columnsFrom": [ + "contest_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "acm_contest_rank_user_id_40391ab2_fk_user_id": { + "name": "acm_contest_rank_user_id_40391ab2_fk_user_id", + "tableFrom": "acm_contest_rank", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_acm_rank_user_contest": { + "name": "unique_acm_rank_user_contest", + "nullsNotDistinct": false, + "columns": [ + "contest_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_analysis": { + "name": "ai_analysis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "ai_analysis_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_prompt": { + "name": "user_prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "analysis": { + "name": "analysis", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_pinned": { + "name": "is_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ai_analysis_user_id_3aa23011": { + "name": "ai_analysis_user_id_3aa23011", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_analysis_user_id_3aa23011_fk_user_id": { + "name": "ai_analysis_user_id_3aa23011_fk_user_id", + "tableFrom": "ai_analysis", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_hint": { + "name": "ai_hint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "ai_hint_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "submission_id": { + "name": "submission_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt_version": { + "name": "prompt_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "diagnosis": { + "name": "diagnosis", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "diagnosis_error": { + "name": "diagnosis_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "filter_attempt": { + "name": "filter_attempt", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "filter_blocked": { + "name": "filter_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "filter_reason": { + "name": "filter_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "helpful": { + "name": "helpful", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "feedback_time": { + "name": "feedback_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ai_hint_submission_id_idx": { + "name": "ai_hint_submission_id_idx", + "columns": [ + { + "expression": "submission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_hint_submission_id_fk_submission_id": { + "name": "ai_hint_submission_id_fk_submission_id", + "tableFrom": "ai_hint", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.announcement": { + "name": "announcement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_update_time": { + "name": "last_update_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "top": { + "name": "top", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "announcement_created_by_id_359ccf50": { + "name": "announcement_created_by_id_359ccf50", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "announcement_list_idx": { + "name": "announcement_list_idx", + "columns": [ + { + "expression": "visible", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "top", + "isExpression": false, + "asc": false, + "nulls": "first" + }, + { + "expression": "create_time", + "isExpression": false, + "asc": false, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "announcement_created_by_id_359ccf50_fk_user_id": { + "name": "announcement_created_by_id_359ccf50_fk_user_id", + "tableFrom": "announcement", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.challenge_success": { + "name": "challenge_success", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "challenge_success_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "problem_id": { + "name": "problem_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "challenge_id": { + "name": "challenge_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submission_id": { + "name": "submission_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "challenge_success_problem_idx": { + "name": "challenge_success_problem_idx", + "columns": [ + { + "expression": "problem_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "challenge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "challenge_success_user_id_fk_user_id": { + "name": "challenge_success_user_id_fk_user_id", + "tableFrom": "challenge_success", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "challenge_success_problem_id_fk_problem_id": { + "name": "challenge_success_problem_id_fk_problem_id", + "tableFrom": "challenge_success", + "tableTo": "problem", + "columnsFrom": [ + "problem_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "challenge_success_submission_id_fk_submission_id": { + "name": "challenge_success_submission_id_fk_submission_id", + "tableFrom": "challenge_success", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "challenge_success_user_challenge_unique": { + "name": "challenge_success_user_challenge_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "problem_id", + "challenge_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.class_lesson": { + "name": "class_lesson", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "class_lesson_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "class_name": { + "name": "class_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "problem_ids": { + "name": "problem_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'code'" + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "class_lesson_class_day_idx": { + "name": "class_lesson_class_day_idx", + "columns": [ + { + "expression": "class_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "class_lesson_created_by_idx": { + "name": "class_lesson_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "class_lesson_created_by_fk_user_id": { + "name": "class_lesson_created_by_fk_user_id", + "tableFrom": "class_lesson", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contest": { + "name": "contest", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_time": { + "name": "start_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_update_time": { + "name": "last_update_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "contest_created_by_id_a763ca7e": { + "name": "contest_created_by_id_a763ca7e", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contest_created_by_id_a763ca7e_fk_user_id": { + "name": "contest_created_by_id_a763ca7e_fk_user_id", + "tableFrom": "contest", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exercise": { + "name": "exercise", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "exercise_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "type": { + "name": "type", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "tutorial_id": { + "name": "tutorial_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "exercise_tutorial_id_6fd04055": { + "name": "exercise_tutorial_id_6fd04055", + "columns": [ + { + "expression": "tutorial_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "exercise_tutorial_id_6fd04055_fk_tutorial_id": { + "name": "exercise_tutorial_id_6fd04055_fk_tutorial_id", + "tableFrom": "exercise", + "tableTo": "tutorial", + "columnsFrom": [ + "tutorial_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exercise_attempt": { + "name": "exercise_attempt", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "exercise_id": { + "name": "exercise_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "wrong_attempts": { + "name": "wrong_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "solved": { + "name": "solved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "attempts_to_solve": { + "name": "attempts_to_solve", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_wrong_answer": { + "name": "last_wrong_answer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_attempt_at": { + "name": "first_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "solved_at": { + "name": "solved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "exercise_attempt_exercise_id_idx": { + "name": "exercise_attempt_exercise_id_idx", + "columns": [ + { + "expression": "exercise_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "exercise_attempt_user_id_fk_user_id": { + "name": "exercise_attempt_user_id_fk_user_id", + "tableFrom": "exercise_attempt", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "exercise_attempt_exercise_id_fk_exercise_id": { + "name": "exercise_attempt_exercise_id_fk_exercise_id", + "tableFrom": "exercise_attempt", + "tableTo": "exercise", + "columnsFrom": [ + "exercise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "exercise_attempt_pkey": { + "name": "exercise_attempt_pkey", + "columns": [ + "user_id", + "exercise_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.flowchart_submission": { + "name": "flowchart_submission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mermaid_code": { + "name": "mermaid_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flowchart_data": { + "name": "flowchart_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ai_score": { + "name": "ai_score", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "ai_grade": { + "name": "ai_grade", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "ai_feedback": { + "name": "ai_feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_suggestions": { + "name": "ai_suggestions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_criteria_details": { + "name": "ai_criteria_details", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "ai_provider": { + "name": "ai_provider", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "ai_model": { + "name": "ai_model", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "processing_time": { + "name": "processing_time", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "evaluation_time": { + "name": "evaluation_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "problem_id": { + "name": "problem_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "flowchart_problem_time_idx": { + "name": "flowchart_problem_time_idx", + "columns": [ + { + "expression": "problem_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + }, + { + "expression": "create_time", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowchart_status_idx": { + "name": "flowchart_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowchart_create_time_idx": { + "name": "flowchart_create_time_idx", + "columns": [ + { + "expression": "create_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "flowchart_user_time_idx": { + "name": "flowchart_user_time_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + }, + { + "expression": "create_time", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "flowchart_submission_problem_id_8551edbf_fk_problem_id": { + "name": "flowchart_submission_problem_id_8551edbf_fk_problem_id", + "tableFrom": "flowchart_submission", + "tableTo": "problem", + "columnsFrom": [ + "problem_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "flowchart_submission_user_id_225c83e8_fk_user_id": { + "name": "flowchart_submission_user_id_225c83e8_fk_user_id", + "tableFrom": "flowchart_submission", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.judge_server": { + "name": "judge_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "judger_version": { + "name": "judger_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cpu_core": { + "name": "cpu_core", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "memory_usage": { + "name": "memory_usage", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "cpu_usage": { + "name": "cpu_usage", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "task_number": { + "name": "task_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "service_url": { + "name": "service_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_disabled": { + "name": "is_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.options_sysoptions": { + "name": "options_sysoptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "options_sysoptions_key_key": { + "name": "options_sysoptions_key_key", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.problem": { + "name": "problem", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_description": { + "name": "input_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "output_description": { + "name": "output_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "samples": { + "name": "samples", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "test_case_id": { + "name": "test_case_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "test_case_score": { + "name": "test_case_score", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "hint": { + "name": "hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "languages": { + "name": "languages", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_update_time": { + "name": "last_update_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "time_limit": { + "name": "time_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "memory_limit": { + "name": "memory_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "difficulty": { + "name": "difficulty", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "submission_number": { + "name": "submission_number", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "accepted_number": { + "name": "accepted_number", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_id": { + "name": "_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "statistic_info": { + "name": "statistic_info", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "contest_id": { + "name": "contest_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "share_submission": { + "name": "share_submission", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "answers": { + "name": "answers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "allow_flowchart": { + "name": "allow_flowchart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "flowchart_data": { + "name": "flowchart_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "flowchart_hint": { + "name": "flowchart_hint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mermaid_code": { + "name": "mermaid_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "show_flowchart": { + "name": "show_flowchart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "ast_rules": { + "name": "ast_rules", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "challenges": { + "name": "challenges", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sql_config": { + "name": "sql_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "sql_display": { + "name": "sql_display", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "problem_contest_visible_idx": { + "name": "problem_contest_visible_idx", + "columns": [ + { + "expression": "contest_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bool_ops" + }, + { + "expression": "visible", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "problem_created_by_id_cb362143": { + "name": "problem_created_by_id_cb362143", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "problem_visible_idx": { + "name": "problem_visible_idx", + "columns": [ + { + "expression": "visible", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bool_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "problem_contest_id_328e013a_fk_contest_id": { + "name": "problem_contest_id_328e013a_fk_contest_id", + "tableFrom": "problem", + "tableTo": "contest", + "columnsFrom": [ + "contest_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "problem_created_by_id_cb362143_fk_user_id": { + "name": "problem_created_by_id_cb362143_fk_user_id", + "tableFrom": "problem", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_problem_id_contest": { + "name": "unique_problem_id_contest", + "nullsNotDistinct": false, + "columns": [ + "_id", + "contest_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.problem_tag": { + "name": "problem_tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'knowledge'" + }, + "category_confirmed": { + "name": "category_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "problem_tag_name_ci_unique": { + "name": "problem_tag_name_ci_unique", + "columns": [ + { + "expression": "lower(name)", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.problem_tags": { + "name": "problem_tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "problem_id": { + "name": "problem_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "problemtag_id": { + "name": "problemtag_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "problem_tags_problemtag_id_72d20571": { + "name": "problem_tags_problemtag_id_72d20571", + "columns": [ + { + "expression": "problemtag_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "problem_tags_problem_id_866ecb8d_fk_problem_id": { + "name": "problem_tags_problem_id_866ecb8d_fk_problem_id", + "tableFrom": "problem_tags", + "tableTo": "problem", + "columnsFrom": [ + "problem_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "problem_tags_problemtag_id_72d20571_fk_problem_tag_id": { + "name": "problem_tags_problemtag_id_72d20571_fk_problem_tag_id", + "tableFrom": "problem_tags", + "tableTo": "problem_tag", + "columnsFrom": [ + "problemtag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "problem_tags_problem_id_problemtag_id_318459d1_uniq": { + "name": "problem_tags_problem_id_problemtag_id_318459d1_uniq", + "nullsNotDistinct": false, + "columns": [ + "problem_id", + "problemtag_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.problemset": { + "name": "problemset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "problemset_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_update_time": { + "name": "last_update_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "problemset_created_by_id_01b5197f": { + "name": "problemset_created_by_id_01b5197f", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "problemset_created_by_id_01b5197f_fk_user_id": { + "name": "problemset_created_by_id_01b5197f_fk_user_id", + "tableFrom": "problemset", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.problemset_badge": { + "name": "problemset_badge", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "problemset_badge_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "condition_type": { + "name": "condition_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "condition_value": { + "name": "condition_value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "problemset_id": { + "name": "problemset_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "problemset_badge_problemset_id_6cb6c74f": { + "name": "problemset_badge_problemset_id_6cb6c74f", + "columns": [ + { + "expression": "problemset_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int8_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "problemset_badge_problemset_id_6cb6c74f_fk_problemset_id": { + "name": "problemset_badge_problemset_id_6cb6c74f_fk_problemset_id", + "tableFrom": "problemset_badge", + "tableTo": "problemset", + "columnsFrom": [ + "problemset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.problemset_problem": { + "name": "problemset_problem", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "problemset_problem_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_required": { + "name": "is_required", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "problem_id": { + "name": "problem_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "problemset_id": { + "name": "problemset_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "problemset_problem_problem_id_fff2d686": { + "name": "problemset_problem_problem_id_fff2d686", + "columns": [ + { + "expression": "problem_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "problemset_problem_problem_id_fff2d686_fk_problem_id": { + "name": "problemset_problem_problem_id_fff2d686_fk_problem_id", + "tableFrom": "problemset_problem", + "tableTo": "problem", + "columnsFrom": [ + "problem_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "problemset_problem_problemset_id_350d17fb_fk_problemset_id": { + "name": "problemset_problem_problemset_id_350d17fb_fk_problemset_id", + "tableFrom": "problemset_problem", + "tableTo": "problemset", + "columnsFrom": [ + "problemset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_problemset_problem": { + "name": "unique_problemset_problem", + "nullsNotDistinct": false, + "columns": [ + "problem_id", + "problemset_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.problemset_progress": { + "name": "problemset_progress", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "problemset_progress_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "join_time": { + "name": "join_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "complete_time": { + "name": "complete_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "is_completed": { + "name": "is_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "completed_problems_count": { + "name": "completed_problems_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_problems_count": { + "name": "total_problems_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "progress_detail": { + "name": "progress_detail", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "problemset_id": { + "name": "problemset_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "problemset_progress_user_id_c8041a80": { + "name": "problemset_progress_user_id_c8041a80", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "problemset_progress_problemset_id_20a9632e_fk_problemset_id": { + "name": "problemset_progress_problemset_id_20a9632e_fk_problemset_id", + "tableFrom": "problemset_progress", + "tableTo": "problemset", + "columnsFrom": [ + "problemset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "problemset_progress_user_id_c8041a80_fk_user_id": { + "name": "problemset_progress_user_id_c8041a80_fk_user_id", + "tableFrom": "problemset_progress", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_problemset_progress_user": { + "name": "unique_problemset_progress_user", + "nullsNotDistinct": false, + "columns": [ + "problemset_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reaction": { + "name": "reaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "reaction_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "type": { + "name": "type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "problem_id": { + "name": "problem_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "reaction_problem_type_idx": { + "name": "reaction_problem_type_idx", + "columns": [ + { + "expression": "problem_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reaction_user_id_cfa7f469": { + "name": "reaction_user_id_cfa7f469", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reaction_problem_id_a7f3b9f3_fk_problem_id": { + "name": "reaction_problem_id_a7f3b9f3_fk_problem_id", + "tableFrom": "reaction", + "tableTo": "problem", + "columnsFrom": [ + "problem_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reaction_user_id_cfa7f469_fk_user_id": { + "name": "reaction_user_id_cfa7f469_fk_user_id", + "tableFrom": "reaction", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reaction_problem_user_unique": { + "name": "reaction_problem_user_unique", + "nullsNotDistinct": false, + "columns": [ + "problem_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submission": { + "name": "submission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "contest_id": { + "name": "contest_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "problem_id": { + "name": "problem_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 6 + }, + "info": { + "name": "info", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shared": { + "name": "shared", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "statistic_info": { + "name": "statistic_info", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "problemset_id": { + "name": "problemset_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "contest_create_time_idx": { + "name": "contest_create_time_idx", + "columns": [ + { + "expression": "contest_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "create_time", + "isExpression": false, + "asc": false, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_public_create_time_id_idx": { + "name": "submission_public_create_time_id_idx", + "columns": [ + { + "expression": "create_time", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"submission\".\"contest_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "problem_user_idx": { + "name": "problem_user_idx", + "columns": [ + { + "expression": "problem_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_result_37e2f67a": { + "name": "submission_result_37e2f67a", + "columns": [ + { + "expression": "result", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_create_time_idx": { + "name": "user_create_time_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + }, + { + "expression": "create_time", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_language_time_idx": { + "name": "submission_language_time_idx", + "columns": [ + { + "expression": "language", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "create_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"submission\".\"contest_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_result_time_idx": { + "name": "submission_result_time_idx", + "columns": [ + { + "expression": "result", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "create_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"submission\".\"contest_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_public_problem_time_idx": { + "name": "submission_public_problem_time_idx", + "columns": [ + { + "expression": "problem_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "create_time", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"submission\".\"contest_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_public_username_trgm_idx": { + "name": "submission_public_username_trgm_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "where": "\"submission\".\"contest_id\" is null", + "concurrently": false, + "method": "gin", + "with": {} + }, + "submission_public_metrics_idx": { + "name": "submission_public_metrics_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "problem_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "result", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "create_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"submission\".\"contest_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_public_problem_user_idx": { + "name": "submission_public_problem_user_idx", + "columns": [ + { + "expression": "problem_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "result", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"submission\".\"contest_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "submission_problemset_id_idx": { + "name": "submission_problemset_id_idx", + "columns": [ + { + "expression": "problemset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"submission\".\"problemset_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "submission_contest_id_775716d5_fk_contest_id": { + "name": "submission_contest_id_775716d5_fk_contest_id", + "tableFrom": "submission", + "tableTo": "contest", + "columnsFrom": [ + "contest_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "submission_problem_id_76847b55_fk_problem_id": { + "name": "submission_problem_id_76847b55_fk_problem_id", + "tableFrom": "submission", + "tableTo": "problem", + "columnsFrom": [ + "problem_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "submission_problemset_id_fk_problemset_id": { + "name": "submission_problemset_id_fk_problemset_id", + "tableFrom": "submission", + "tableTo": "problemset", + "columnsFrom": [ + "problemset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submission_trace": { + "name": "submission_trace", + "schema": "", + "columns": { + "submission_id": { + "name": "submission_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "active_ms": { + "name": "active_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "since_open_ms": { + "name": "since_open_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "typed_chars": { + "name": "typed_chars", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pasted_chars": { + "name": "pasted_chars", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "paste_count": { + "name": "paste_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "max_paste": { + "name": "max_paste", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_chars": { + "name": "deleted_chars", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "blur_count": { + "name": "blur_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "initial_len": { + "name": "initial_len", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "collab": { + "name": "collab", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "since_prev_ms": { + "name": "since_prev_ms", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "submission_trace_submission_id_fk_submission_id": { + "name": "submission_trace_submission_id_fk_submission_id", + "tableFrom": "submission_trace", + "tableTo": "submission", + "columnsFrom": [ + "submission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tutorial": { + "name": "tutorial", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "tutorial_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "title": { + "name": "title", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "project": { + "name": "project", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": { + "tutorial_created_by_id_07973cab": { + "name": "tutorial_created_by_id_07973cab", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tutorial_created_by_id_07973cab_fk_user_id": { + "name": "tutorial_created_by_id_07973cab_fk_user_id", + "tableFrom": "tutorial", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tutorial_problem": { + "name": "tutorial_problem", + "schema": "", + "columns": { + "tutorial_id": { + "name": "tutorial_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "problem_id": { + "name": "problem_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "tutorial_problem_tutorial_id_fk_tutorial_id": { + "name": "tutorial_problem_tutorial_id_fk_tutorial_id", + "tableFrom": "tutorial_problem", + "tableTo": "tutorial", + "columnsFrom": [ + "tutorial_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tutorial_problem_problem_id_fk_problem_id": { + "name": "tutorial_problem_problem_id_fk_problem_id", + "tableFrom": "tutorial_problem", + "tableTo": "problem", + "columnsFrom": [ + "problem_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tutorial_problem_pkey": { + "name": "tutorial_problem_pkey", + "columns": [ + "tutorial_id", + "problem_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tutorial_progress": { + "name": "tutorial_progress", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tutorial_id": { + "name": "tutorial_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "view_count": { + "name": "view_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_seconds": { + "name": "total_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_viewed_at": { + "name": "first_viewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_viewed_at": { + "name": "last_viewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tutorial_progress_tutorial_id_idx": { + "name": "tutorial_progress_tutorial_id_idx", + "columns": [ + { + "expression": "tutorial_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tutorial_progress_user_id_fk_user_id": { + "name": "tutorial_progress_user_id_fk_user_id", + "tableFrom": "tutorial_progress", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tutorial_progress_tutorial_id_fk_tutorial_id": { + "name": "tutorial_progress_tutorial_id_fk_tutorial_id", + "tableFrom": "tutorial_progress", + "tableTo": "tutorial", + "columnsFrom": [ + "tutorial_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tutorial_progress_pkey": { + "name": "tutorial_progress_pkey", + "columns": [ + "user_id", + "tutorial_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "password": { + "name": "password", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "last_login": { + "name": "last_login", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "create_time": { + "name": "create_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "admin_type": { + "name": "admin_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_disabled": { + "name": "is_disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "problem_permission": { + "name": "problem_permission", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "raw_password": { + "name": "raw_password", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "class_name": { + "name": "class_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rank_hidden_at": { + "name": "rank_hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "token_version": { + "name": "token_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "user_active_idx": { + "name": "user_active_idx", + "columns": [ + { + "expression": "is_disabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_login", + "isExpression": false, + "asc": false, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_class_name_idx": { + "name": "user_class_name_idx", + "columns": [ + { + "expression": "class_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_username_key": { + "name": "user_username_key", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_achievement": { + "name": "user_achievement", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "user_achievement_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "unlock_time": { + "name": "unlock_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "backfilled": { + "name": "backfilled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notified": { + "name": "notified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "achievement_id": { + "name": "achievement_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_achievement_achievement_id_29db600d": { + "name": "user_achievement_achievement_id_29db600d", + "columns": [ + { + "expression": "achievement_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int8_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_achv_notified_idx": { + "name": "user_achv_notified_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + }, + { + "expression": "notified", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "bool_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_achv_time_idx": { + "name": "user_achv_time_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int4_ops" + }, + { + "expression": "unlock_time", + "isExpression": false, + "asc": false, + "nulls": "first", + "opclass": "timestamptz_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_achievement_achievement_id_29db600d_fk_achievement_id": { + "name": "user_achievement_achievement_id_29db600d_fk_achievement_id", + "tableFrom": "user_achievement", + "tableTo": "achievement", + "columnsFrom": [ + "achievement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_achievement_user_id_b8ec7d6a_fk_user_id": { + "name": "user_achievement_user_id_b8ec7d6a_fk_user_id", + "tableFrom": "user_achievement", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_user_achievement": { + "name": "unique_user_achievement", + "nullsNotDistinct": false, + "columns": [ + "achievement_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_badge": { + "name": "user_badge", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "user_badge_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "earned_time": { + "name": "earned_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "badge_id": { + "name": "badge_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_badge_badge_id_92a983e9": { + "name": "user_badge_badge_id_92a983e9", + "columns": [ + { + "expression": "badge_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "int8_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_badge_badge_id_92a983e9_fk_problemset_badge_id": { + "name": "user_badge_badge_id_92a983e9_fk_problemset_badge_id", + "tableFrom": "user_badge", + "tableTo": "problemset_badge", + "columnsFrom": [ + "badge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_badge_user_id_a286d718_fk_user_id": { + "name": "user_badge_user_id_a286d718_fk_user_id", + "tableFrom": "user_badge", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_user_badge": { + "name": "unique_user_badge", + "nullsNotDistinct": false, + "columns": [ + "badge_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profile": { + "name": "user_profile", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "acm_problems_status": { + "name": "acm_problems_status", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mood": { + "name": "mood", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_number": { + "name": "accepted_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "submission_number": { + "name": "submission_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "real_name": { + "name": "real_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_profile_user_id_8fdce8e2_fk_user_id": { + "name": "user_profile_user_id_8fdce8e2_fk_user_id", + "tableFrom": "user_profile", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_profile_user_id_key": { + "name": "user_profile_user_id_key", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stat": { + "name": "user_stat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "user_stat_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "update_time": { + "name": "update_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_stat_user_id_73337fc0_fk_user_id": { + "name": "user_stat_user_id_73337fc0_fk_user_id", + "tableFrom": "user_stat", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stat_user_id_key": { + "name": "user_stat_user_id_key", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/src/db/meta/_journal.json b/apps/api/src/db/meta/_journal.json index b2ea5c8..b0c223c 100644 --- a/apps/api/src/db/meta/_journal.json +++ b/apps/api/src/db/meta/_journal.json @@ -288,6 +288,13 @@ "when": 1793622303180, "tag": "0040_class_lesson_many_per_day", "breakpoints": true + }, + { + "idx": 41, + "version": "7", + "when": 1793626595966, + "tag": "0041_lesson_mode", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 31b3963..4dc968d 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -45,6 +45,7 @@ import type { HintDiagnosis, JudgeStatus, LessonLanguage, + LessonMode, ProblemChallenges, ProblemDifficulty, ProblemLanguage, @@ -1324,6 +1325,11 @@ export const classLesson = pgTable( * 没有它。写入侧在 PUT /classroom/lesson 按 lessonLanguageSchema 校验过 */ language: text().$type(), + /** + * 这节课做什么:写代码 / 画流程图 / 先画再写(见契约 lessonModeSchema)。加这一列之前布置的 + * 都是写代码。写入侧按 lessonModeSchema 校验过 + */ + mode: text().$type().notNull().default('code'), /** 这一节第一次布置的老师,别的老师「改」也不换(和 createdAt 一样)。看板上写「HH:mm 谁 布置」 */ createdBy: integer('created_by').notNull(), /** diff --git a/apps/api/src/routes/admin/lesson.ts b/apps/api/src/routes/admin/lesson.ts index 3f91d4f..9871d93 100644 --- a/apps/api/src/routes/admin/lesson.ts +++ b/apps/api/src/routes/admin/lesson.ts @@ -34,6 +34,7 @@ adminLessonRoutes.get('/lessons', requireTeacher, async (c) => { day: schema.classLesson.day, problemIds: schema.classLesson.problemIds, language: schema.classLesson.language, + mode: schema.classLesson.mode, createdAt: schema.classLesson.createdAt, updatedAt: schema.classLesson.updatedAt, createdBy: teacherName, @@ -85,6 +86,7 @@ adminLessonRoutes.get('/lessons', requireTeacher, async (c) => { updatedAt: row.updatedAt, createdBy: row.createdBy, language: row.language ?? null, + mode: row.mode, // 题被删掉的(problem_ids 不挂外键)直接不列 problems: row.problemIds.flatMap((id) => { const problem = problemById.get(id) @@ -98,23 +100,33 @@ adminLessonRoutes.get('/lessons', requireTeacher, async (c) => { } satisfies AdminLessonList) }) -/** 新补一节、改一节共用的校验:班得有人、题号都对得上、语言每道都能交 */ -async function lessonValues( - body: AdminLessonRequest, -): Promise< +/** 新补一节、改一节共用的校验:班得有人、题号都对得上、语言每道都能交、要画的都打开了画流程图 */ +async function lessonValues(body: AdminLessonRequest): Promise< | { code: string; message: string } - | { values: { className: string; day: string; problemIds: number[]; language: AdminLessonRequest['language']; createdAt: string } } + | { + values: { + className: string + day: string + problemIds: number[] + language: AdminLessonRequest['language'] + mode: AdminLessonRequest['mode'] + createdAt: string + } + } > { const [exists] = await db.select({ id: schema.user.id }).from(schema.user).where(eq(schema.user.className, body.className)).limit(1) if (!exists) return { code: 'class-not-found', message: `没有 ${body.className} 这个班` } - const resolved = await resolveLessonProblems(body.problemDisplayIds, body.language) + // 只画流程图不用语言,存成 null(和看板布置一样) + const language = body.mode === 'flowchart' ? null : body.language + const resolved = await resolveLessonProblems(body.problemDisplayIds, language, body.mode) if ('code' in resolved) return resolved return { values: { className: body.className, day: body.day, problemIds: resolved.problemIds, - language: body.language, + language, + mode: body.mode, createdAt: localDateTime(body.day, body.time), }, } diff --git a/apps/api/src/routes/classroom.ts b/apps/api/src/routes/classroom.ts index 3273af3..809eee7 100644 --- a/apps/api/src/routes/classroom.ts +++ b/apps/api/src/routes/classroom.ts @@ -9,6 +9,8 @@ import { type ClassActivityProblem, type LastVisit, type LessonLanguage, + type LessonMode, + lessonNeeds, type StatisticInfo, } from '@oj2/contract' import { and, asc, desc, eq, gte, inArray, isNotNull, isNull, lt, sql } from 'drizzle-orm' @@ -24,7 +26,7 @@ import { failure, parseBody, success } from '../http' import { isAccepted } from '../judge/status' import { activityRows, classMemberIds, FLOWCHART_PASSED, lessonLanguageFilter, SOLVED_RESULTS, suggestActiveClass } from '../services/class-activity' import { accepted } from '../services/learning-stats' -import { announceLessonChange, resolveLessonProblems, todayLessonSnapshot } from '../services/lesson' +import { announceLessonChange, lessonDoneCounts, resolveLessonProblems, todayLessonSnapshot } from '../services/lesson' import { calendarDay, dayStart, localTime } from '../time' export const classroomRoutes = new Hono() @@ -50,30 +52,49 @@ interface ClassDayGroup { * 某个班从 `since` 起、按(东八区日, 题)分组的做题人数和通过人数。只算正常状态的学生, * 不含比赛提交,代码和流程图合起来数(见 activityRows)。`minUsers` 是「班里在做」的门槛, * 给了 `problemIds`(老师布置的题)时不设门槛 —— 那几道题是确定的,一个人都没交也要列出来。 + * + * 给了 `mode`(老师布置的这节做什么):写代码只数代码、画流程图只数流程图;先画再写两种都数, + * 「通过」要同一个人两样都做到 —— 所以先按人归一遍再数 */ async function classDayGroups( className: string, since: string, - options: { minUsers?: number; problemIds?: number[]; language?: LessonLanguage | null } = {}, + options: { minUsers?: number; problemIds?: number[]; language?: LessonLanguage | null; mode?: LessonMode | null } = {}, ): Promise { + const { mode } = options const activity = activityRows({ since, users: classMemberIds(className), problemIds: options.problemIds, language: options.language, + only: mode === 'code' || mode === 'flowchart' ? mode : undefined, }) const day = sql`to_char(${localTime(activity.createTime)}, 'YYYY-MM-DD')` - const userCount = sql`count(distinct ${activity.userId})::int` - const query = db + const perUser = db .select({ - day, - problemId: activity.problemId, - firstAt: sql`min(${activity.createTime})`, - userCount, - acceptedCount: sql`count(distinct ${activity.userId}) filter (where ${activity.solved})::int`, + day: day.as('day'), + problemId: sql`${activity.problemId}`.as('problem_id'), + userId: sql`${activity.userId}`.as('user_id'), + firstAt: sql`min(${activity.createTime})`.as('first_at'), + solved: (mode === 'both' + ? sql`bool_or(${activity.kind} = 'code' and ${activity.solved}) and bool_or(${activity.kind} = 'flowchart' and ${activity.solved})` + : sql`bool_or(${activity.solved})` + ).as('solved'), }) .from(activity) - .groupBy(day, activity.problemId) + .groupBy(day, activity.problemId, activity.userId) + .as('per_user') + const userCount = sql`count(*)::int` + const query = db + .select({ + day: perUser.day, + problemId: perUser.problemId, + firstAt: sql`min(${perUser.firstAt})`, + userCount, + acceptedCount: sql`(count(*) filter (where ${perUser.solved}))::int`, + }) + .from(perUser) + .groupBy(perUser.day, perUser.problemId) return options.minUsers ? query.having(sql`${userCount} >= ${options.minUsers}`) : query } @@ -93,7 +114,7 @@ async function visibleProblems(ids: number[]) { } /** - * 老师给这个班今天布置的题(problem.id,按输入顺序)、选的语言、布置时刻和布置的老师; + * 老师给这个班今天布置的题(problem.id,按输入顺序)、选的语言和做什么、布置时刻和布置的老师; * 没布置为 null。一天布置过几节的,取最后布置的那一节。老师写真名:老师之间认的是名字,用户名(lwp0201、little)认不出是谁 */ async function lessonPlan(className: string, day: string) { @@ -102,6 +123,7 @@ async function lessonPlan(className: string, day: string) { id: schema.classLesson.id, problemIds: schema.classLesson.problemIds, language: schema.classLesson.language, + mode: schema.classLesson.mode, createdAt: schema.classLesson.createdAt, createdBy: sql`coalesce(nullif(${schema.userProfile.realName}, ''), ${schema.user.username})`, }) @@ -129,7 +151,7 @@ async function classLessonProblems(className: string, lookbackDays: number) { const planned = plan.problemIds const [problems, groups] = await Promise.all([ visibleProblems(planned), - classDayGroups(className, dayStart(), { problemIds: planned, language: plan.language }), + classDayGroups(className, dayStart(), { problemIds: planned, language: plan.language, mode: plan.mode }), ]) const groupById = new Map(groups.map((row) => [row.problemId, row])) return { @@ -137,6 +159,7 @@ async function classLessonProblems(className: string, lookbackDays: number) { lessonId: plan.id, day: today, language: plan.language ?? null, + mode: plan.mode, assignedAt: plan.createdAt, assignedBy: plan.createdBy, problems: planned.flatMap((id) => { @@ -166,8 +189,9 @@ async function classLessonProblems(className: string, lookbackDays: number) { source: picked.length ? ('inferred' as const) : null, lessonId: null, day: picked.length ? latest : null, - // 推断出来的课不知道老师要的是哪种语言,不限 + // 推断出来的课不知道老师要的是哪种语言、要画还是要写,不限 language: null, + mode: null, assignedAt: null, assignedBy: null, problems: picked.flatMap((row) => { @@ -298,12 +322,15 @@ classroomRoutes.get('/me/class-activity', requireAuth, async (c) => { day: null, source: null, language: null, + mode: null, problems: [], } satisfies ClassActivity) } const lesson = await classLessonProblems(user.className, CLASS_ACTIVITY_LOOKBACK_DAYS) const ids = lesson.problems.map((problem) => problem.id) + const steps = lesson.source === 'teacher' && lesson.mode ? await todaySteps(user.className, ids, lesson) : null + const codeLanguage = steps ? steps.language : lesson.language // 代码提交和流程图提交两张表,互不依赖,一起查 const [mine, drawn] = await Promise.all([ ids.length @@ -319,14 +346,12 @@ classroomRoutes.get('/me/class-activity', requireAuth, async (c) => { inArray(schema.submission.problemId, ids), isNull(schema.submission.contestId), // 布置的是 C:以前用 Python 做对过不算这份作业做完 - lessonLanguageFilter(lesson.language), + lessonLanguageFilter(codeLanguage), ), ) .groupBy(schema.submission.problemId) : [], - // 流程图作业:画到 A / S 也算做完(设计文档 2026-09-28-problem-page-redesign 第 3 节决定 6)。 - // 有的题流程图交了几百次、代码个位数,只看代码提交的话,这些学生在课堂条上永远是「没做」。 - // 交过就算「做过」,评分中、评失败的也算 —— 和代码提交编译错误也算「做过」同一个口径 + // 流程图:交过就算「做过」,评分中、评失败的也算 —— 和代码提交编译错误也算「做过」同一个口径 ids.length ? db .select({ @@ -338,29 +363,92 @@ classroomRoutes.get('/me/class-activity', requireAuth, async (c) => { .groupBy(schema.flowchartSubmission.problemId) : [], ]) - const mineById = new Map(mine.map((row) => [row.problemId, row.accepted])) - for (const row of drawn) { - mineById.set(row.problemId, (mineById.get(row.problemId) ?? false) || row.passed) - } + type Step = 'accepted' | 'tried' | 'none' + const stepOf = (value: boolean | undefined): Step => (value === undefined ? 'none' : value ? 'accepted' : 'tried') + const codeById = new Map(mine.map((row) => [row.problemId, row.accepted])) + const drawById = new Map(drawn.map((row) => [row.problemId, row.passed])) return success(c, { className: user.className, day: lesson.day, source: lesson.source, - language: lesson.language, + language: codeLanguage, + mode: lesson.mode, problems: lesson.problems.map((problem) => { - const accepted = mineById.get(problem.id) + const code = stepOf(codeById.get(problem.id)) + const draw = stepOf(drawById.get(problem.id)) + const needs = steps?.needs.get(problem.id) + let myStatus: Step + if (needs) { + // 老师布置的:要哪几步就看哪几步,全做到才算做完 + const wanted = [needs.draw ? draw : null, needs.code ? code : null].filter((step): step is Step => step !== null) + myStatus = wanted.every((step) => step === 'accepted') ? 'accepted' : wanted.some((step) => step !== 'none') ? 'tried' : 'none' + } else { + // 推断出来的课:代码交对或流程图画到 A / S 都算(有的题全班在画流程图,只看代码的话永远是「没做」) + myStatus = code === 'accepted' || draw === 'accepted' ? 'accepted' : code === 'tried' || draw === 'tried' ? 'tried' : 'none' + } return { problemDisplayId: problem.displayId, title: problem.title, userCount: problem.userCount, acceptedCount: problem.acceptedCount, - myStatus: accepted === undefined ? 'none' : accepted ? 'accepted' : 'tried', + myStatus, + draw: needs?.draw ? draw : null, + code: needs?.code ? code : null, } satisfies ClassActivityProblem }), } satisfies ClassActivity) }) +/** + * 学生那边一道题要画还是要写:今天这个班布置过的每一节里,凡是有这道题的,要求合起来算。 + * 老师先布置一节画流程图、再布置一节同样的题写代码时,还没画完的人照样看得到要画 —— + * 学生只看得到今天最后布置的那一节,不合起来的话,画流程图那节一布置写代码就没了。 + * + * 写代码用的语言:今天最后布置的、要写代码的那一节选的(只画流程图的课没有语言) + */ +async function todaySteps( + className: string, + problemIds: number[], + latest: { lessonId: number | null; language: LessonLanguage | null; mode: LessonMode | null }, +) { + const rows = await db + .select({ id: schema.classLesson.id, problemIds: schema.classLesson.problemIds, language: schema.classLesson.language, mode: schema.classLesson.mode }) + .from(schema.classLesson) + .where(and(eq(schema.classLesson.className, className), eq(schema.classLesson.day, calendarDay()))) + .orderBy(desc(schema.classLesson.createdAt), desc(schema.classLesson.id)) + const needs = new Map() + let language: LessonLanguage | null = latest.mode && lessonNeeds(latest.mode).code ? latest.language : null + let languageFound = !!latest.mode && lessonNeeds(latest.mode).code + for (const row of rows) { + const shared = row.id === latest.lessonId ? problemIds : row.problemIds.filter((id) => problemIds.includes(id)) + if (!shared.length) continue + const rowNeeds = lessonNeeds(row.mode) + for (const id of shared) { + const known = needs.get(id) ?? { draw: false, code: false } + needs.set(id, { draw: known.draw || rowNeeds.draw, code: known.code || rowNeeds.code }) + } + if (!languageFound && rowNeeds.code) { + language = row.language ?? null + languageFound = true + } + } + return { needs, language } +} + +/** + * 一张流程图走对了几组、一共几组:2026-10 起的「走例子」报告(ai_criteria_details version 2, + * 形状见契约 FlowchartCaseReport)里数。老评法、还没评完的、评失败的都是 null + */ +function flowchartCaseCounts() { + const details = schema.flowchartSubmission.aiCriteriaDetails + const isReport = sql`${schema.flowchartSubmission.status} = 2 and ${details}->>'version' = '2' and jsonb_typeof(${details}->'cases') = 'array'` + return { + passed: sql`case when ${isReport} then (select count(*) from jsonb_array_elements(${details}->'cases') e where e->>'ok' = 'true')::int end`, + total: sql`case when ${isReport} then jsonb_array_length(${details}->'cases') end`, + } +} + /** * 看板上的「最近几节课交了几节」:分清「今天没来」和「一直不动手」。2025 秋至少上过 * 5 节课的学生里,12.5% 七成以上的课一道都没交,37% 的学生贡献了一半以上的「没交」 @@ -385,6 +473,7 @@ classroomRoutes.get('/classroom/board', requireTeacher, async (c) => { source: null, lessonId: null, language: null, + mode: null, assignedAt: null, assignedBy: null, lastLanguage: null, @@ -438,7 +527,10 @@ classroomRoutes.get('/classroom/board', requireTeacher, async (c) => { const challengeDoneKeys = new Set( challengeDone.filter((row) => liveChallenges.get(row.problemId)?.has(row.challengeId)).map((row) => `${row.userId}:${row.problemId}`), ) - const [cells, lastSubmits, lastDrawn, attendance, drawn, todays, seen] = await Promise.all([ + // 老师布置的这节做什么;推断出来的课为 null,代码交对或流程图画对都算(和加「做什么」之前一样) + const mode = lesson.source === 'teacher' ? lesson.mode : null + const needs = mode ? lessonNeeds(mode) : null + const [cells, lastSubmits, lastDrawn, attendance, drawn, todays, seen, lastFlows] = await Promise.all([ ids.length && userIds.length ? db .select({ @@ -537,7 +629,27 @@ classroomRoutes.get('/classroom/board', requireTeacher, async (c) => { .orderBy(schema.submission.createTime) : [], seenToday(userIds), + /** + * 要画流程图的课:每人每题最后一张图(不限今天,和 drawn 的 firstPassedAt 同一个范围), + * 还没画对的格子点开讲它哪里错、按走对几组归堆。走对几组从 2026-10 起的「走例子」报告里数, + * 老评法和还没评完的为 null + */ + needs?.draw && ids.length && userIds.length + ? db + .selectDistinctOn([schema.flowchartSubmission.userId, schema.flowchartSubmission.problemId], { + userId: schema.flowchartSubmission.userId, + problemId: schema.flowchartSubmission.problemId, + id: schema.flowchartSubmission.id, + status: schema.flowchartSubmission.status, + grade: schema.flowchartSubmission.aiGrade, + ...flowchartCaseCounts(), + }) + .from(schema.flowchartSubmission) + .where(and(inArray(schema.flowchartSubmission.userId, userIds), inArray(schema.flowchartSubmission.problemId, ids))) + .orderBy(schema.flowchartSubmission.userId, schema.flowchartSubmission.problemId, desc(schema.flowchartSubmission.createTime)) + : [], ]) + const lastFlowByKey = new Map(lastFlows.map((row) => [`${row.userId}:${row.problemId}`, row])) const todayByKey = Map.groupBy(todays, (row) => `${row.userId}:${row.problemId}`) const cellByKey = new Map(cells.map((row) => [`${row.userId}:${row.problemId}`, row])) const drawnByKey = new Map(drawn.map((row) => [`${row.userId}:${row.problemId}`, row])) @@ -549,6 +661,8 @@ classroomRoutes.get('/classroom/board', requireTeacher, async (c) => { if (!known || Date.parse(row.lastAt) > Date.parse(known)) lastByUser.set(row.userId, row.lastAt) } const earliest = (a: string | null | undefined, b: string | null | undefined) => (!a ? (b ?? null) : !b ? a : Date.parse(a) <= Date.parse(b) ? a : b) + const latestOf = (a: string, b: string) => (Date.parse(a) >= Date.parse(b) ? a : b) + type Step = 'accepted' | 'tried' | 'none' const attendedByUser = new Map(attendance.map((row) => [row.userId, row.days])) return success(c, { @@ -557,6 +671,7 @@ classroomRoutes.get('/classroom/board', requireTeacher, async (c) => { source: lesson.source, lessonId: lesson.lessonId, language: lesson.language, + mode, assignedAt: lesson.assignedAt, assignedBy: lesson.assignedBy, lastLanguage: last?.language ?? null, @@ -577,16 +692,50 @@ classroomRoutes.get('/classroom/board', requireTeacher, async (c) => { const key = `${student.userId}:${id}` const cell = cellByKey.get(key) const flow = drawnByKey.get(key) - const acceptedAt = earliest(cell?.firstAcceptedAt, flow?.firstPassedAt) - const today = todayByKey.get(key) ?? [] - const latest = acceptedAt ? undefined : today.at(-1) - const firstTry = acceptedAt && Date.parse(acceptedAt) >= Date.parse(start) ? today.findIndex((row) => isAccepted(row.result)) : -1 + const codeAt = cell?.firstAcceptedAt ?? null + const flowAt = flow?.firstPassedAt ?? null + const codeStatus: Step = codeAt ? 'accepted' : cell ? 'tried' : 'none' + const drawStatus: Step = flowAt ? 'accepted' : flow ? 'tried' : 'none' + let status: Step + let acceptedAt: string | null + let attempts: number + if (!needs) { + acceptedAt = earliest(codeAt, flowAt) + status = !cell && !flow ? 'none' : acceptedAt ? 'accepted' : 'tried' + attempts = (cell?.attempts ?? 0) + (flow?.attempts ?? 0) + } else { + // 要哪几步看哪几步:两步都做到的时刻是晚的那一步 + const steps = [needs.draw ? { status: drawStatus, at: flowAt } : null, needs.code ? { status: codeStatus, at: codeAt } : null].filter( + (step) => step !== null, + ) + acceptedAt = steps.every((step) => step.at) ? steps.map((step) => step.at!).reduce(latestOf) : null + status = acceptedAt ? 'accepted' : steps.some((step) => step.status !== 'none') ? 'tried' : 'none' + attempts = (needs.code ? (cell?.attempts ?? 0) : 0) + (needs.draw ? (flow?.attempts ?? 0) : 0) + } + // 下面几样都是代码那一步的:只画流程图的课没有 + const codeWanted = !needs || needs.code + const today = codeWanted ? (todayByKey.get(key) ?? []) : [] + const codeDone = needs ? codeAt : acceptedAt + const latest = codeDone ? undefined : today.at(-1) + const firstTry = codeDone && Date.parse(codeDone) >= Date.parse(start) ? today.findIndex((row) => isAccepted(row.result)) : -1 + const lastFlow = lastFlowByKey.get(key) return { - status: !cell && !flow ? 'none' : acceptedAt ? 'accepted' : 'tried', - attempts: (cell?.attempts ?? 0) + (flow?.attempts ?? 0), + status, + attempts, codeAttempts: cell?.attempts ?? 0, flowchartAttempts: flow?.attempts ?? 0, acceptedAt, + codeStatus, + flowchart: needs?.draw + ? { + status: drawStatus, + passedAt: flowAt, + latest: + !flowAt && lastFlow + ? { id: lastFlow.id, status: lastFlow.status, grade: lastFlow.grade, passed: lastFlow.passed, total: lastFlow.total } + : null, + } + : null, latest: latest ? { submissionId: latest.id, @@ -595,7 +744,7 @@ classroomRoutes.get('/classroom/board', requireTeacher, async (c) => { sampleCheck: latest.sampleCheck ?? undefined, } : null, - trail: acceptedAt + trail: codeDone ? today.map((row) => ({ id: row.id, result: row.result, @@ -604,11 +753,11 @@ classroomRoutes.get('/classroom/board', requireTeacher, async (c) => { : [], acceptedTry: firstTry >= 0 ? firstTry + 1 : null, challenge: - !acceptedAt || !liveChallenges.get(id)?.size + !codeWanted || !codeDone || !liveChallenges.get(id)?.size ? null : challengeDoneKeys.has(key) ? 'done' - : today.some((row) => Date.parse(row.createTime) > Date.parse(acceptedAt)) + : today.some((row) => Date.parse(row.createTime) > Date.parse(codeDone)) ? 'doing' : null, } @@ -660,13 +809,18 @@ classroomRoutes.get('/classroom/today', requireTeacher, async (c) => { ]) const ids = lesson.problems.map((problem) => problem.id) let allDoneCount = 0 - if (ids.length) { - // 做没做对不限今天(和看板的格子一样:以前做对的也算做完),语言跟着布置走 + if (ids.length && lesson.source === 'teacher' && lesson.lessonId && lesson.mode) { + // 老师布置的:和看板、后台课堂作业同一个口径(要画的看图、要写的看代码) + const counts = await lessonDoneCounts([ + { id: lesson.lessonId, className, day: calendarDay(), problemIds: ids, language: lesson.language, mode: lesson.mode }, + ]) + allDoneCount = counts.get(lesson.lessonId) ?? 0 + } else if (ids.length) { + // 推断出来的:做没做对不限今天(和看板的格子一样:以前做对的也算做完),代码或流程图都算 const done = activityRows({ since: new Date(0).toISOString(), users: classMemberIds(className), problemIds: ids, - language: lesson.language, }) const rows = await db .select({ @@ -739,7 +893,9 @@ classroomRoutes.put('/classroom/lesson', requireTeacher, async (c) => { const parsed = await parseBody(c, classLessonRequestSchema, '题号或语言不对') if (!parsed.success) return parsed.response const user = c.get('user')! - const { className, problemDisplayIds, language, lessonId, newLesson } = parsed.data + const { className, problemDisplayIds, lessonId, newLesson, mode } = parsed.data + // 只画流程图不用语言:存成 null,看板和学生那边就不会写「C语言」 + const language = mode === 'flowchart' ? null : parsed.data.language const day = calendarDay() const [exists] = await db.select({ id: schema.user.id }).from(schema.user).where(eq(schema.user.className, className)).limit(1) @@ -768,13 +924,13 @@ classroomRoutes.put('/classroom/lesson', requireTeacher, async (c) => { return success(c, null) } - const resolved = await resolveLessonProblems(problemDisplayIds, language) + const resolved = await resolveLessonProblems(problemDisplayIds, language, mode) if ('code' in resolved) return failure(c, 400, resolved.code, resolved.message) const { problemIds } = resolved const now = new Date().toISOString() if (target) { // 「改」不换布置的人,和 createdAt 一样认第一次 - await db.update(schema.classLesson).set({ problemIds, language, updatedAt: now }).where(eq(schema.classLesson.id, target.id)) + await db.update(schema.classLesson).set({ problemIds, language, mode, updatedAt: now }).where(eq(schema.classLesson.id, target.id)) } else { // 新开一节、今天还没有,或者点名的那一节已经没了(别的老师清掉了)—— 最后这种也照新开处理, // 老师手上的题号不至于白填 @@ -783,6 +939,7 @@ classroomRoutes.put('/classroom/lesson', requireTeacher, async (c) => { day, problemIds, language, + mode, createdBy: user.id, createdAt: now, updatedAt: now, diff --git a/apps/api/src/routes/problem.ts b/apps/api/src/routes/problem.ts index e5b448e..fa5ce93 100644 --- a/apps/api/src/routes/problem.ts +++ b/apps/api/src/routes/problem.ts @@ -81,6 +81,11 @@ function lessonLanguageFilter(raw: string | undefined) { return language ? sql`${schema.problem.languages} @> ${JSON.stringify([language])}::jsonb` : undefined } +/** 要画流程图的课(`?flowchart=1`):只列打开了「让学生自己画」的题,没打开的学生那边没有画布 */ +function lessonFlowchartFilter(raw: string | undefined) { + return raw === '1' ? eq(schema.problem.allowFlowchart, true) : undefined +} + type ProblemStates = Awaited> | null type UserCounts = Awaited> @@ -149,6 +154,8 @@ problemRoutes.get('/problems', optionalAuth, async (c) => { if (byType) filters.push(byType) const byLanguage = lessonLanguageFilter(c.req.query('language')) if (byLanguage) filters.push(byLanguage) + const byFlowchart = lessonFlowchartFilter(c.req.query('flowchart')) + if (byFlowchart) filters.push(byFlowchart) const states = user ? await problemStates(user.id) : null if (undone && states && states.solved.size > 0) filters.push(notInArray(schema.problem.id, [...states.solved])) @@ -226,6 +233,7 @@ problemRoutes.get('/problem-tags', async (c) => { eq(schema.problem.visible, true), isNull(schema.problem.contestId), lessonLanguageFilter(c.req.query('language')), + lessonFlowchartFilter(c.req.query('flowchart')), ), ) .where(keyword ? ilike(schema.problemTag.name, `%${keyword}%`) : undefined) diff --git a/apps/api/src/routes/submission-statistics.ts b/apps/api/src/routes/submission-statistics.ts index bac0b25..2992a37 100644 --- a/apps/api/src/routes/submission-statistics.ts +++ b/apps/api/src/routes/submission-statistics.ts @@ -7,6 +7,7 @@ */ import { + FLOWCHART_PASS_GRADES, NO_CLASS, STUDENT_ROLES, type ProblemLanguage, @@ -537,6 +538,7 @@ async function lessonPlans(range: StatisticsRange) { day: schema.classLesson.day, problemIds: schema.classLesson.problemIds, language: schema.classLesson.language, + mode: schema.classLesson.mode, createdAt: schema.classLesson.createdAt, }) .from(schema.classLesson) @@ -560,6 +562,7 @@ async function assignedLessons(plans: LessonPlan[], range: StatisticsRange) { class_name: plan.className, day: plan.day, problem_id: problemId, + mode: plan.mode, })), ), ) @@ -571,7 +574,7 @@ async function assignedLessons(plans: LessonPlan[], range: StatisticsRange) { UNJUDGED_RESULTS.map((value) => sql`${value}`), sql`, `, ) - const plan = sql`(select * from jsonb_to_recordset(${planRows}::jsonb) as p(k int, class_name text, day text, problem_id int))` + const plan = sql`(select * from jsonb_to_recordset(${planRows}::jsonb) as p(k int, class_name text, day text, problem_id int, mode text))` const [counts, doneByLesson] = await Promise.all([ db.execute<{ k: number @@ -584,22 +587,37 @@ async function assignedLessons(plans: LessonPlan[], range: StatisticsRange) { last: string }>(sql` with p as ${plan}, + -- 这节课要写代码就数代码提交,要画流程图就数流程图(画到 A / S 算对、还在评算没判完), + -- 先画再写两种都数。同一天先布置画流程图、再布置同样的题写代码,两节各数各的 s as ( - select p.k, s.user_id, s.problem_id, s.result, s.create_time + select p.k, s.user_id, s.problem_id, s.result in (${accepted}) as ok, s.result in (${judging}) as pending, s.create_time from ${schema.submission} s join ${schema.user} u on u.id = s.user_id join p on p.class_name = u.class_name and p.problem_id = s.problem_id and p.day = to_char(${localTime(sql`s.create_time`)}, 'YYYY-MM-DD') - where s.contest_id is null + where s.contest_id is null and p.mode <> 'flowchart' and s.create_time <= ${range.end} ${range.start ? sql`and s.create_time >= ${range.start}` : sql``} and u.admin_type = 'Regular User' and u.is_disabled = false + union all + select p.k, f.user_id, f.problem_id, (f.status = 2 and coalesce(f.ai_grade in (${sql.join( + FLOWCHART_PASS_GRADES.map((grade) => sql`${grade}`), + sql`, `, + )}), false)) as ok, f.status in (0, 1) as pending, f.create_time + from ${schema.flowchartSubmission} f + join ${schema.user} u on u.id = f.user_id + join p on p.class_name = u.class_name and p.problem_id = f.problem_id + and p.day = to_char(${localTime(sql`f.create_time`)}, 'YYYY-MM-DD') + where p.mode <> 'code' + and f.create_time <= ${range.end} + ${range.start ? sql`and f.create_time >= ${range.start}` : sql``} + and u.admin_type = 'Regular User' and u.is_disabled = false ) select k, problem_id, count(distinct user_id)::int as users, count(*)::int as total, - (count(*) filter (where result in (${accepted})))::int as accepted, - (count(*) filter (where result in (${judging})))::int as judging, + (count(*) filter (where ok))::int as accepted, + (count(*) filter (where pending))::int as judging, min(create_time) as first, max(create_time) as last from s @@ -624,6 +642,7 @@ async function assignedLessons(plans: LessonPlan[], range: StatisticsRange) { className: lesson.className, day: lesson.day, problemIds: lesson.problemIds, + mode: lesson.mode, start: first, end: last, users: total?.users ?? 0, @@ -868,6 +887,7 @@ submissionStatisticsRoutes.get('/submissions/statistics/lessons', requireTeacher .map((row) => row!.displayId) .slice(0, STATISTICS_MAX_PROBLEMS), assigned: false, + mode: null, doneCount: null, fromProblemSet: lesson.from_ps * 2 > lesson.total, // 次数和正确率只算这节课的题:点进去筛的就是这几道,两边的数才对得上 @@ -901,6 +921,7 @@ submissionStatisticsRoutes.get('/submissions/statistics/lessons', requireTeacher .map((row) => row!.displayId) .slice(0, STATISTICS_MAX_PROBLEMS), assigned: true, + mode: lesson.mode, doneCount: lesson.doneCount, fromProblemSet: false, total: lesson.total, diff --git a/apps/api/src/services/class-activity.ts b/apps/api/src/services/class-activity.ts index 6c7c346..32a909d 100644 --- a/apps/api/src/services/class-activity.ts +++ b/apps/api/src/services/class-activity.ts @@ -53,14 +53,20 @@ export function activityRows(options: { problemIds?: number[] /** 老师布置时选了语言:代码只数这个语言交的(见 lessonLanguageFilter),流程图不受影响 */ language?: LessonLanguage | null + /** + * 只要哪一种:老师布置的课选了「写代码」就只数代码、「画流程图」就只数流程图 + * (契约 lessonModeSchema)。不给 = 两种都数,`kind` 列分得出是哪种 + */ + only?: 'code' | 'flowchart' }) { - const { since, users, problemIds, language } = options + const { since, users, problemIds, language, only } = options const code = db .select({ userId: schema.submission.userId, problemId: schema.submission.problemId, createTime: schema.submission.createTime, solved: sql`${inArray(schema.submission.result, SOLVED_RESULTS)}`.as('solved'), + kind: sql<'code' | 'flowchart'>`'code'`.as('kind'), }) .from(schema.submission) .where( @@ -70,6 +76,7 @@ export function activityRows(options: { users ? inArray(schema.submission.userId, users) : undefined, problemIds ? inArray(schema.submission.problemId, problemIds) : undefined, lessonLanguageFilter(language), + only === 'flowchart' ? sql`false` : undefined, ), ) const flowchart = db @@ -78,6 +85,7 @@ export function activityRows(options: { problemId: schema.flowchartSubmission.problemId, createTime: schema.flowchartSubmission.createTime, solved: sql`${FLOWCHART_PASSED}`.as('solved'), + kind: sql<'code' | 'flowchart'>`'flowchart'`.as('kind'), }) .from(schema.flowchartSubmission) .where( @@ -85,6 +93,7 @@ export function activityRows(options: { gte(schema.flowchartSubmission.createTime, since), users ? inArray(schema.flowchartSubmission.userId, users) : undefined, problemIds ? inArray(schema.flowchartSubmission.problemId, problemIds) : undefined, + only === 'code' ? sql`false` : undefined, ), ) return code.unionAll(flowchart).as('activity') diff --git a/apps/api/src/services/lesson.ts b/apps/api/src/services/lesson.ts index face69c..8c5232c 100644 --- a/apps/api/src/services/lesson.ts +++ b/apps/api/src/services/lesson.ts @@ -1,4 +1,4 @@ -import { isSqlProblem, STUDENT_ROLES, type LessonLanguage, type LessonUpdate } from '@oj2/contract' +import { isSqlProblem, lessonNeeds, STUDENT_ROLES, type LessonLanguage, type LessonMode, type LessonUpdate } from '@oj2/contract' import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm' import { db, schema } from '../db' @@ -13,26 +13,34 @@ import { FLOWCHART_PASSED, SOLVED_RESULTS } from './class-activity' /** * 题号 → problem.id,保留输入顺序、去重、不分大小写。只认题库里公开的题;有对不上的整个拒掉并 - * 点名是哪几个,免得存下半张单子老师还不知道。选了语言就得每道都能用它交(SQL 题除外:它只收 - * SQL,C 的作业里夹一道也不会让人选错语言,计数那边也放行了 SQL) + * 点名是哪几个,免得存下半张单子老师还不知道。要写代码的课选了语言就得每道都能用它交(SQL 题除外: + * 它只收 SQL,C 的作业里夹一道也不会让人选错语言,计数那边也放行了 SQL);要画流程图的课 + * 每道都得打开了「让学生自己画」,不然学生打开题目根本没有画布 */ export async function resolveLessonProblems( problemDisplayIds: string[], language: LessonLanguage | null, -): Promise<{ problemIds: number[] } | { code: 'problem-not-found' | 'language-not-allowed'; message: string }> { + mode: LessonMode = 'code', +): Promise<{ problemIds: number[] } | { code: 'problem-not-found' | 'language-not-allowed' | 'flowchart-not-allowed'; message: string }> { const wanted = [...new Set(problemDisplayIds.map((id) => id.toLowerCase()))] const found = await db .select({ id: schema.problem.id, displayId: schema.problem.displayId, languages: schema.problem.languages, + allowFlowchart: schema.problem.allowFlowchart, }) .from(schema.problem) .where(and(inArray(sql`lower(${schema.problem.displayId})`, wanted), eq(schema.problem.visible, true), isNull(schema.problem.contestId))) const idByDisplay = new Map(found.map((row) => [row.displayId.toLowerCase(), row.id])) const missing = wanted.filter((id) => !idByDisplay.has(id)) if (missing.length) return { code: 'problem-not-found', message: `这些题号不存在或没有公开:${missing.join('、')}` } - if (language) { + const needs = lessonNeeds(mode) + if (needs.draw) { + const undrawable = found.filter((row) => !row.allowFlowchart).map((row) => row.displayId) + if (undrawable.length) return { code: 'flowchart-not-allowed', message: `这些题没有打开画流程图:${undrawable.join('、')}` } + } + if (language && needs.code) { const unsupported = found.filter((row) => !row.languages.includes(language) && !isSqlProblem(row)).map((row) => row.displayId) if (unsupported.length) return { code: 'language-not-allowed', message: `这些题不能用 ${language} 交:${unsupported.join('、')}` } } @@ -45,17 +53,21 @@ export function lessonDayEnd(day: string) { } /** - * 每节布置的课,班里把布置的题全做对了的人数:那天结束前做对过布置的每一道(以前做对的也算), - * 代码只认布置的语言(SQL 题放行),流程图评到 A / S 也算 —— 和看板的格子同一个口径。 + * 每节布置的课,班里把布置的题全做对了的人数:那天结束前做对过布置的每一道(以前做对的也算)。 + * 「做对」看这节课做什么(契约 lessonModeSchema):写代码只认代码交对,代码只认布置的语言 + * (SQL 题放行);画流程图只认评到 A / S;先画再写两样都要 —— 和看板的格子同一个口径。 * 人按花名册(STUDENT_ROLES、没禁用),和「全班几人」对得上 */ -export async function lessonDoneCounts(lessons: { id: number; className: string; day: string; problemIds: number[]; language: LessonLanguage | null }[]) { +export async function lessonDoneCounts( + lessons: { id: number; className: string; day: string; problemIds: number[]; language: LessonLanguage | null; mode: LessonMode }[], +) { const rows = lessons.flatMap((lesson) => lesson.problemIds.map((problemId) => ({ k: lesson.id, class_name: lesson.className, problem_id: problemId, language: lesson.language, + mode: lesson.mode, until: lessonDayEnd(lesson.day), })), ) @@ -66,10 +78,10 @@ export async function lessonDoneCounts(lessons: { id: number; className: string; ) const result = await db.execute<{ k: number; done: number }>(sql` with p as ( - select * from jsonb_to_recordset(${JSON.stringify(rows)}::jsonb) as p(k int, class_name text, problem_id int, language text, until timestamptz) + select * from jsonb_to_recordset(${JSON.stringify(rows)}::jsonb) as p(k int, class_name text, problem_id int, language text, mode text, until timestamptz) ), members as ( - select p.k, p.problem_id, p.language, p.until, u.id as user_id + select p.k, p.problem_id, p.language, p.mode, p.until, u.id as user_id from p join ${schema.user} u on u.class_name = p.class_name where u.is_disabled = false and u.admin_type in (${sql.join( STUDENT_ROLES.map((role) => sql`${role}`), @@ -79,16 +91,16 @@ export async function lessonDoneCounts(lessons: { id: number; className: string; solved as ( select m.k, m.user_id, m.problem_id from members m - where exists ( + where (m.mode = 'flowchart' or exists ( select 1 from ${schema.submission} s where s.user_id = m.user_id and s.problem_id = m.problem_id and s.contest_id is null and s.result in (${solved}) and s.create_time < m.until and (m.language is null or s.language in (m.language, 'SQL')) - ) or exists ( + )) and (m.mode = 'code' or exists ( select 1 from ${schema.flowchartSubmission} where ${schema.flowchartSubmission.userId} = m.user_id and ${schema.flowchartSubmission.problemId} = m.problem_id and ${FLOWCHART_PASSED} and ${schema.flowchartSubmission.createTime} < m.until - ) + )) ), per_user as ( select k, user_id from solved group by k, user_id @@ -103,6 +115,7 @@ interface LessonSnapshot { id: number problemIds: number[] language: LessonLanguage | null + mode: LessonMode } /** @@ -111,7 +124,7 @@ interface LessonSnapshot { */ export async function todayLessonSnapshot(className: string): Promise { const [row] = await db - .select({ id: schema.classLesson.id, problemIds: schema.classLesson.problemIds, language: schema.classLesson.language }) + .select({ id: schema.classLesson.id, problemIds: schema.classLesson.problemIds, language: schema.classLesson.language, mode: schema.classLesson.mode }) .from(schema.classLesson) .where(and(eq(schema.classLesson.className, className), eq(schema.classLesson.day, calendarDay()))) .orderBy(desc(schema.classLesson.createdAt), desc(schema.classLesson.id)) @@ -128,7 +141,8 @@ export async function announceLessonChange(className: string, before: LessonSnap const after = await todayLessonSnapshot(className) if (!before && !after) return const sameLesson = !!before && !!after && before.id === after.id - if (sameLesson && before.language === after.language && before.problemIds.join() === after.problemIds.join()) return + const sameWay = sameLesson && before.language === after.language && before.mode === after.mode + if (sameWay && before.problemIds.join() === after.problemIds.join()) return const ids = [...new Set([...(before?.problemIds ?? []), ...(after?.problemIds ?? [])])] const rows = ids.length @@ -150,10 +164,11 @@ export async function announceLessonChange(className: string, before: LessonSnap type: 'lesson_update', kind: !after ? 'cleared' : sameLesson ? 'changed' : 'assigned', language: after?.language ?? null, + mode: after?.mode ?? null, problems: pick(afterIds), added: sameLesson ? pick(added) : [], removed: pick(removed), - languageChanged: sameLesson && before.language !== after.language, + languageChanged: sameLesson && !sameWay, reordered: sameLesson && kept.join() !== keptBefore.join(), } satisfies LessonUpdate) } catch (error) { diff --git a/apps/web/src/admin/lesson/components/LessonEditModal.vue b/apps/web/src/admin/lesson/components/LessonEditModal.vue index 1165f0f..4d84703 100644 --- a/apps/web/src/admin/lesson/components/LessonEditModal.vue +++ b/apps/web/src/admin/lesson/components/LessonEditModal.vue @@ -1,6 +1,6 @@ + + + + diff --git a/apps/web/src/oj/classroom/components/LessonForm.vue b/apps/web/src/oj/classroom/components/LessonForm.vue index 033b3f0..db2f5aa 100644 --- a/apps/web/src/oj/classroom/components/LessonForm.vue +++ b/apps/web/src/oj/classroom/components/LessonForm.vue @@ -1,5 +1,5 @@ @@ -236,14 +247,14 @@ function goCode() {
{{ current.grade }} 级 {{ flowchartVerdict(report, current.grade) }} - 这道题算做完了 + {{ codeStillNeeded ? '接着写代码' : '这道题算做完了' }}
{{ current.score }} 分 {{ current.grade }} 级
diff --git a/apps/web/src/oj/problem/components/ProblemChips.vue b/apps/web/src/oj/problem/components/ProblemChips.vue index eee308c..2d3fc7d 100644 --- a/apps/web/src/oj/problem/components/ProblemChips.vue +++ b/apps/web/src/oj/problem/components/ProblemChips.vue @@ -15,6 +15,8 @@ export interface ChipItem { title: string status: 'done' | 'tried' | 'none' to: RouteLocationRaw + /** 悬停时代替「做过,还没对」那句(课堂里要画又要写的题,说差哪一步) */ + note?: string } const props = defineProps<{ @@ -61,7 +63,7 @@ watch( :to="item.to" class="chip" :class="[item.status, { current: isCurrent(item) }]" - :aria-label="`${item.displayId} ${item.title}(${STATUS_TEXT[item.status]})`" + :aria-label="`${item.displayId} ${item.title}(${item.note ?? STATUS_TEXT[item.status]})`" :aria-current="isCurrent(item) ? 'page' : undefined" > @@ -109,7 +111,7 @@ watch( {{ item.displayId }} - {{ item.displayId }} {{ item.title }} · {{ STATUS_TEXT[item.status] }} + {{ item.displayId }} {{ item.title }} · {{ item.note ?? STATUS_TEXT[item.status] }} diff --git a/apps/web/src/oj/problem/components/ProblemEditor.vue b/apps/web/src/oj/problem/components/ProblemEditor.vue index cb8fd19..65e6a76 100644 --- a/apps/web/src/oj/problem/components/ProblemEditor.vue +++ b/apps/web/src/oj/problem/components/ProblemEditor.vue @@ -172,20 +172,38 @@ watch( * 编辑器停在默认的 Python(或者上一个用这台电脑的人留下的偏好)的话,学生交上去才发现 * 不算数。课堂条只在题库入口出现,这里同一个范围。 * - * 每道题只自动换一次:学生自己再切回别的语言,就按他的来。课堂状态可能比题面晚到 - * (课堂条进页面才去拉),所以两边都等到了再判断;画着流程图时不动他。 + * 要画流程图的课(画流程图 / 先画再写,或者今天先后布置了画和写):这道题的图还没画对, + * 就停在画布上;画对了、还要写代码的,换到布置的语言 —— 画一道写一道、全画完再回来写, + * 打开题目都是该做的那一步。 + * + * 每道题、每一节课只自动换一次:学生自己再切回别的,就按他的来。老师换了一节(做法、语言变了) + * 再判断一次。画对图的那一刻不跟着换:他正在看点评,「照着它写代码」由他自己点。 + * 课堂状态可能比题面晚到(课堂条进页面才去拉),所以两边都等到了再判断; + * 没有画这一步的课,画着流程图时不动他 */ const lessonStore = useLessonStore() let lessonLanguageFor: string | null = null watch( () => [problem.value?._id, lessonStore.activity] as const, ([id, activity]) => { - if (!id || !activity || lessonLanguageFor === id) return - lessonLanguageFor = id + if (!id || !activity) return + const key = [id, activity.day, activity.mode, activity.language].join('|') + if (lessonLanguageFor === key) return + lessonLanguageFor = key + if (ctx.value.entry !== 'problem' || teacherCollab.value) return + const item = lessonStore.find(id) + if (!item) return + if (item.draw && item.draw !== 'accepted') { + if (problemStore.canDraw && codeStore.code.language !== 'Flowchart') problemStore.switchLanguage('Flowchart') + return + } + // 只画流程图、已经画对了:没有要写的 + if (item.draw && !item.code) return const language = activity.language - if (!language || ctx.value.entry !== 'problem' || teacherCollab.value) return - if (!lessonStore.includes(id) || !problemStore.codeLanguages.includes(language)) return - if (codeStore.code.language === language || codeStore.code.language === 'Flowchart') return + if (!language || !problemStore.codeLanguages.includes(language)) return + if (codeStore.code.language === language) return + // 没有画这一步、自己在画流程图的,不打断他 + if (codeStore.code.language === 'Flowchart' && !item.draw) return problemStore.switchLanguage(language) }, { immediate: true }, diff --git a/apps/web/src/oj/statistics/components/Lessons.vue b/apps/web/src/oj/statistics/components/Lessons.vue index bd79e64..177f2c3 100644 --- a/apps/web/src/oj/statistics/components/Lessons.vue +++ b/apps/web/src/oj/statistics/components/Lessons.vue @@ -1,4 +1,5 @@