change query

This commit is contained in:
2026-07-20 11:28:21 +08:00
parent b401c1b53a
commit a2c9936f79
5 changed files with 49 additions and 53 deletions
+17 -42
View File
@@ -210,56 +210,31 @@ const isRequirementDone = (r, signals) => {
// ─── Helper: resolve which sibling task(s) are blocking a locked task ───────
// Prerequisites are always siblings within the same task list (enforced by
// admin's syncTaskPrerequisites), so names can always be resolved from `arr`.
// Mirrors the locked boolean rule above: explicit prereqIds win when present,
// otherwise falls back to earlier is_required tasks in order_index order.
// A task with no explicit task_prerequisites rows is never locked.
const resolveLockedBy = (task, i, arr, prereqsByTask, completedById) => {
const prereqIds = prereqsByTask.get(task.task_id);
const blockers = (prereqIds && prereqIds.length)
? arr.filter((t) => prereqIds.includes(t.task_id) && completedById.get(t.task_id) !== true)
: arr.slice(0, i).filter((prev) => prev.is_required && !completedById.get(prev.task_id));
if (!prereqIds || !prereqIds.length) return [];
const blockers = arr.filter((t) => prereqIds.includes(t.task_id) && completedById.get(t.task_id) !== true);
return blockers.map((t) => ({ task_id: t.task_id, name: t.name }));
};
// ─── Helper: explicit prerequisite gate ─────────────────────────────────────
// Returns true/false when `taskId` has explicit task_prerequisites rows —
// ALL of them must be completed by this user. Returns null when the task has
// no explicit prerequisites configured, signaling the caller to fall back to
// the default order_index/is_required linear sequencing below.
// ALL of them must be completed by this user. Returns true (unlocked) when
// the task has no explicit prerequisites configured.
const checkPrerequisitesUnlocked = async (userId, taskId) => {
const prereqRows = await TaskPrerequisite.findAll({ where: { task_id: taskId } });
if (!prereqRows.length) return null;
if (!prereqRows.length) return true;
const results = await Promise.all(prereqRows.map((r) => checkTaskCompletion(userId, r.prerequisite_task_id)));
return results.every(Boolean);
};
// ─── Helper: server-side sequencing gate ───────────────────────────────────
// Prefers a task's explicit prerequisites (see checkPrerequisitesUnlocked)
// when configured; otherwise falls back to the original rule — rejects a
// completion write if any earlier *required* task in the same list isn't
// complete yet. Shared by this file's submitTask and
// task_progress.controller.js's visitLink/updateProgress.
const assertTaskUnlocked = async (userId, taskListId, orderIndex, taskId) => {
if (taskId) {
const prereqResult = await checkPrerequisitesUnlocked(userId, taskId);
if (prereqResult !== null) return prereqResult;
}
const earlierRequired = await Task.findAll({
where: { task_list_id: taskListId, order_index: { [Op.lt]: orderIndex }, is_required: true },
include: [{ model: TaskRequirement, as: 'requirements' }],
});
if (!earlierRequired.length) return true;
const taskIds = earlierRequired.map((t) => t.task_id);
const allRequirements = earlierRequired.flatMap((t) => (t.requirements ?? []).map((r) => r.toJSON()));
const signals = await getTaskCompletionSignals(userId, taskIds, allRequirements);
return earlierRequired.every((t) => {
const reqs = (t.requirements ?? []);
return reqs.length > 0 && reqs.every((r) => isRequirementDone(r.toJSON ? r.toJSON() : r, signals));
});
};
// A task is locked only by its own explicit task_prerequisites rows — no
// implicit locking based on list position. Shared by this file's submitTask
// and task_progress.controller.js's visitLink/updateProgress.
const assertTaskUnlocked = async (userId, taskId) => checkPrerequisitesUnlocked(userId, taskId);
// ─── Helper: is this one task fully done for this user, right now? ─────────
const checkTaskCompletion = async (userId, taskId) => {
@@ -407,8 +382,8 @@ exports.getGroupTaskList = async (req, res) => {
}
// ── Bucket + lock each task — a task with explicit prerequisites is
// locked until ALL of them are done; otherwise fall back to the linear
// order_index/is_required rule (same rule assertTaskUnlocked enforces).
// locked until ALL of them are done; a task with none is never locked
// (same rule assertTaskUnlocked enforces).
const bucketedTasks = tasks.map((task, i, arr) => {
const has_completed = completedById.get(task.task_id);
@@ -547,8 +522,8 @@ exports.getGroupTaskLists = async (req, res) => {
task.has_completed = computeHasCompleted(task);
});
// has_completed lookup scoped to THIS list's tasks (order_index
// fallback only ever looks at siblings within the same list).
// has_completed lookup scoped to THIS list's tasks (prerequisite
// edges only ever point at siblings within the same list).
const completedById = new Map(tasks.map((task) => [task.task_id, task.has_completed]));
tasks.forEach((task, i, arr) => {
task.lockedBy = resolveLockedBy(task, i, arr, prereqsByTask, completedById);
@@ -655,7 +630,7 @@ exports.getTask = async (req, res) => {
const data = task.toJSON();
data.latest_completion = data.completions?.[0] ?? null;
delete data.completions;
data.locked = !(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index, taskId));
data.locked = !(await assertTaskUnlocked(req.user.user_id, taskId));
return R.success(res, 'Task retrieved.', data);
} catch (err) {
@@ -728,9 +703,9 @@ exports.submitTask = async (req, res) => {
return R.error(res, 'This task no longer accepts submissions.', 409);
}
if (!(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index, taskId))) {
if (!(await assertTaskUnlocked(req.user.user_id, taskId))) {
await t.rollback();
return R.error(res, 'Complete the earlier required tasks in this list first.', 409);
return R.error(res, 'Complete this task\'s prerequisite tasks first.', 409);
}
const wasComplete = await checkTaskCompletion(req.user.user_id, taskId);
@@ -218,9 +218,9 @@ exports.visitLink = async (req, res) => {
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId }, transaction: t });
if (!task) { await t.rollback(); return R.error(res, 'Task not found.', 404); }
if (!(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index, taskId))) {
if (!(await assertTaskUnlocked(req.user.user_id, taskId))) {
await t.rollback();
return R.error(res, 'Complete the earlier required tasks in this list first.', 409);
return R.error(res, 'Complete this task\'s prerequisite tasks first.', 409);
}
const wasComplete = await checkTaskCompletion(req.user.user_id, taskId);
@@ -349,9 +349,9 @@ exports.updateProgress = async (req, res) => {
const task = await Task.findOne({ where: { task_id: taskId, task_list_id: taskListId }, transaction: t });
if (!task) { await t.rollback(); return R.error(res, 'Task not found.', 404); }
if (!(await assertTaskUnlocked(req.user.user_id, taskListId, task.order_index, taskId))) {
if (!(await assertTaskUnlocked(req.user.user_id, taskId))) {
await t.rollback();
return R.error(res, 'Complete the earlier required tasks in this list first.', 409);
return R.error(res, 'Complete this task\'s prerequisite tasks first.', 409);
}
const now = new Date();