duidui_mini_program/pages/camp_detail/modules/data-fetcher.js
2026-03-27 10:41:46 +08:00

398 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 数据获取模块
* 负责所有数据获取相关的逻辑
*/
import { campApi } from '../../../config/camp_api.js';
import { generateExpandedStatus } from './utils.js';
/**
* 顺序调用 purchaseSection 开启多个小节,完成后静默刷新营地数据(用于前一小节完成后自动开启后一小节)
*/
function openSectionsThenRefresh(context, campId, sectionIds, index, silent) {
if (index >= sectionIds.length) {
fetchCampData(context, campId, true);
return;
}
var sectionId = sectionIds[index];
campApi.purchaseSection(campId, sectionId)
.then(function () {
openSectionsThenRefresh(context, campId, sectionIds, index + 1, silent);
})
.catch(function () {
openSectionsThenRefresh(context, campId, sectionIds, index + 1, silent);
});
}
/**
* 当上一小节已完成、下一小节未购买时,请求后端 canUnlockSection若允许且免费则加入自动开启列表最后统一 openSectionsThenRefresh
*/
function checkAndAutoOpenNextSections(context, campId, courseList, startIndex, toOpen, silent) {
if (startIndex >= courseList.length) {
if (toOpen.length > 0) {
openSectionsThenRefresh(context, campId, toOpen, 0, silent);
}
return Promise.resolve();
}
var prev = courseList[startIndex - 1];
var curr = courseList[startIndex];
if (!prev.is_completed || curr.is_purchased) {
return checkAndAutoOpenNextSections(context, campId, courseList, startIndex + 1, toOpen, silent);
}
return campApi.canUnlockSection(campId, curr.id)
.then(function (res) {
var ok = res && (res.success === true || res.code === 200);
var canUnlock = ok && (res.can_unlock === true);
var isFree = (curr.price_fen == null || curr.price_fen === undefined || curr.price_fen <= 0);
if (canUnlock && isFree) {
toOpen.push(curr.id);
}
return checkAndAutoOpenNextSections(context, campId, courseList, startIndex + 1, toOpen, silent);
})
.catch(function () {
return checkAndAutoOpenNextSections(context, campId, courseList, startIndex + 1, toOpen, silent);
});
}
/**
* 为「未购买」或「有时间间隔且非第一节」的小节请求 can_unlock_section若返回 can_unlock=false 且 unlock_at 则写入 sectionUnlockAtMap 供页面倒计时展示并禁止进入
* 注意:已购买但配置了时间间隔的小节也可能未到解锁时间(如 1 小时未到),需同样请求以展示倒计时并拦截点击
*/
function fetchUnlockAtForLockedSections(context, campId, courseList) {
var map = {};
var promises = [];
for (var i = 0; i < courseList.length; i++) {
var course = courseList[i];
var needUnlockCheck = !course.is_purchased ||
(course.time_interval_type && course.time_interval_value > 0 && (course.section_number || 0) > 1);
if (!needUnlockCheck) { continue; }
(function (sectionId) {
promises.push(
campApi.canUnlockSection(campId, sectionId).then(function (res) {
var unlockAt = res && (res.unlock_at !== undefined && res.unlock_at !== null) ? parseInt(res.unlock_at, 10) : null;
if (unlockAt && !isNaN(unlockAt) && res.can_unlock === false) {
map[sectionId] = unlockAt;
}
}).catch(function () {})
);
})(course.id);
}
Promise.all(promises).then(function () {
if (Object.keys(map).length > 0) {
context.setData({ sectionUnlockAtMap: map });
if (typeof context.startUnlockCountdownTimer === 'function') {
context.startUnlockCountdownTimer();
}
}
});
}
/**
* 轻量级刷新任务状态(已废弃)
* 注意:此函数已不再使用,因为聚合接口 camp_detail_with_user_status 已经返回了完整的任务状态
* 如果需要更新任务状态,请使用 fetchCampData 重新加载完整数据
* @deprecated 不再使用单独的 progress/list 接口
*/
export function refreshTaskStatus(context) {
// 此函数已废弃,不再执行任何操作
// 如果需要刷新任务状态,请调用 fetchCampData 重新加载完整数据
}
/**
* 获取所有营地数据(仅在首次加载时使用)
* @param {Object} context - 页面上下文
* @param {String} campId - 打卡营ID
* @param {Boolean} silent - 是否静默更新(不显示 loading
*/
export function fetchCampData(context, campId, silent) {
if (!campId) {
wx.showToast({
title: '营地ID不能为空',
icon: 'none'
});
return;
}
if (silent !== true) {
context.setData({ loading: true });
}
var userId = wx.getStorageSync('wxuserid') || '';
// 使用新的聚合接口
campApi.getCampDetailWithUserStatus(campId, userId)
.then(function (res) {
if (!res || res.success !== true) {
throw new Error(res && res.message || '获取数据失败');
}
var campDetail = res.camp_detail || {};
var camp = campDetail.camp || {};
var userCamp = campDetail.user_camp || {};
var sections = res.sections || [];
// 处理介绍类型和内容
var introTypeNormalized = (function(raw) {
if (raw === null || raw === undefined) return 'INTRO_TYPE_NONE';
if (typeof raw === 'number') {
if (raw === 2) return 'INTRO_TYPE_VIDEO';
if (raw === 1) return 'INTRO_TYPE_IMAGE_TEXT';
return 'INTRO_TYPE_NONE';
}
var str = String(raw).toUpperCase();
if (str === '2' || str.indexOf('VIDEO') !== -1) return 'INTRO_TYPE_VIDEO';
if (str === '1' || str.indexOf('IMAGE_TEXT') !== -1 || str.indexOf('IMAGETEXT') !== -1) return 'INTRO_TYPE_IMAGE_TEXT';
return 'INTRO_TYPE_NONE';
})(camp.intro_type);
var introContent = camp.intro_content || '';
var isVideo = introTypeNormalized === 'INTRO_TYPE_VIDEO' && introContent;
// 获取当前学习的小节ID
var currentSectionId = null;
if (userCamp.is_joined === true && userCamp.current_section_id) {
currentSectionId = String(userCamp.current_section_id);
}
// 转换打卡营状态
var campStatus = 'NotStarted';
if (userCamp.camp_status) {
var statusStr = String(userCamp.camp_status).toUpperCase();
if (statusStr.indexOf('COMPLETED') !== -1) {
campStatus = 'Completed';
} else if (statusStr.indexOf('IN_PROGRESS') !== -1 || statusStr.indexOf('INPROGRESS') !== -1) {
campStatus = 'InProgress';
}
}
// 构建访问列表(从 sections 中提取已购买的小节)
var accessList = [];
sections.forEach(function (sec) {
if (sec.is_purchased === true) {
accessList.push({
section_id: sec.id
});
}
});
// 处理小节列表sections 已经包含任务列表和进度信息)
var courseList = sections.map(function (sec) {
var tasks = sec.tasks || [];
// 处理每个任务的状态,确保审核被拒绝的任务正确显示
tasks.forEach(function(task) {
if (!task) return;
// 兼容后端解锁关系:若有 prerequisite_task_id 但无 prerequisites则生成数组供点击逻辑使用
if ((!task.prerequisites || task.prerequisites.length === 0) && task.prerequisite_task_id) {
task.prerequisites = [task.prerequisite_task_id];
}
if (!task.prerequisites) {
task.prerequisites = [];
}
// 判断任务类型(只有主观题和申论题需要审核)
var taskType = String(task.task_type || '').toLowerCase();
var isReviewableTask = taskType === 'subjective' || taskType === 'essay' || task.need_review === true;
// 只有需要审核的任务才检查 review_status
if (isReviewableTask && task.progress && task.progress.review_status) {
var reviewStatus = task.progress.review_status;
var reviewStatusUpper = String(reviewStatus).toUpperCase();
// 如果审核状态是 rejected无论其他状态如何都应该显示为 Rejected
if (reviewStatusUpper === 'REJECTED' || reviewStatus === 'rejected' ||
reviewStatusUpper === 'REVIEW_STATUS_REJECTED') {
task.status = 'Rejected';
} else if (reviewStatusUpper === 'APPROVED' || reviewStatus === 'approved' ||
reviewStatusUpper === 'REVIEW_STATUS_APPROVED') {
// 审核通过,如果已完成则显示 Completed否则显示 InProgress
if (task.progress.is_completed === true || task.progress.is_completed === 1) {
task.status = 'Completed';
} else {
task.status = 'InProgress';
}
} else if (reviewStatusUpper === 'PENDING' || reviewStatus === 'pending' ||
reviewStatusUpper === 'REVIEW_STATUS_PENDING') {
// 待审核状态
if (task.progress.is_completed === true || task.progress.is_completed === 1) {
task.status = 'Reviewing';
} else {
task.status = 'InProgress';
}
}
}
// 规范化状态值(确保大小写正确)
if (task.status) {
var normalizedStatus = String(task.status).trim();
if (normalizedStatus === 'NotStarted' || normalizedStatus === 'InProgress' ||
normalizedStatus === 'Completed' || normalizedStatus === 'Reviewing' ||
normalizedStatus === 'Rejected') {
task.status = normalizedStatus;
} else {
// 如果状态值不在预期范围内,尝试转换
var statusUpper = normalizedStatus.toUpperCase();
if (statusUpper.indexOf('COMPLETED') !== -1) {
task.status = 'Completed';
} else if (statusUpper.indexOf('REVIEWING') !== -1) {
task.status = 'Reviewing';
} else if (statusUpper.indexOf('REJECTED') !== -1) {
task.status = 'Rejected';
} else if (statusUpper.indexOf('INPROGRESS') !== -1 || statusUpper.indexOf('IN_PROGRESS') !== -1) {
task.status = 'InProgress';
} else if (statusUpper.indexOf('NOTSTARTED') !== -1 || statusUpper.indexOf('NOT_STARTED') !== -1) {
task.status = 'NotStarted';
} else {
// 无法识别,默认为 NotStarted
task.status = 'NotStarted';
}
}
} else {
// 如果没有 status 字段,默认为 NotStarted
task.status = 'NotStarted';
}
});
// 根据实际的 tasks 数组重新计算 section_progress确保数据正确
var totalTasks = tasks.length;
var completedTasks = 0;
// 统计已完成的任务数(状态为 Completed 的任务)
tasks.forEach(function(task) {
if (task.status === 'Completed') {
completedTasks++;
}
});
// 重新计算 section_progress
var sectionProgress = {
section_id: sec.id,
total_tasks: totalTasks,
completed_tasks: completedTasks,
is_completed: totalTasks > 0 && completedTasks === totalTasks
};
// 如果后端返回的 is_completed 状态与计算的不一致,使用计算的结果
var isCompleted = sectionProgress.is_completed;
return {
id: sec.id,
title: sec.title,
section_number: sec.section_number,
price_fen: sec.price_fen,
is_purchased: sec.is_purchased || false,
is_started: sec.is_started || false,
is_completed: isCompleted,
is_current: sec.is_current || false,
require_previous_section: sec.require_previous_section || false,
time_interval_type: sec.time_interval_type,
time_interval_value: sec.time_interval_value,
tasks: tasks,
section_progress: sectionProgress
};
});
// 查找并维护当前小节信息(优先使用 is_current其次使用 current_section_id
var currentSection = null;
if (courseList && courseList.length > 0) {
// 优先通过 is_current 查找
currentSection = courseList.find(function (sec) {
return sec.is_current === true;
});
// 如果通过 is_current 找不到,则通过 current_section_id 查找
if (!currentSection && currentSectionId) {
currentSection = courseList.find(function (sec) {
return String(sec.id) === String(currentSectionId) ||
String(sec.section_id) === String(currentSectionId) ||
String(sec.course_id) === String(currentSectionId);
});
}
if (currentSection) {
}
}
// 检查当前是否正在播放任务视频
// 如果正在播放任务视频,保留视频播放状态,不要重置
var currentPlayingTaskId = context.data.currentPlayingTaskId;
var currentPlayingTaskType = context.data.currentPlayingTaskType;
var isPlayingTaskVideo = currentPlayingTaskId && currentPlayingTaskType === 'TASK_TYPE_VIDEO';
var shouldShowVideo = isVideo || (isPlayingTaskVideo && context.data.showVideo === true);
// 构建返回数据
var newData = {
loading: silent === true ? context.data.loading : false,
hasJoined: userCamp.is_joined || false,
showVideo: shouldShowVideo,
topContent: isPlayingTaskVideo ? context.data.topContent : introContent,
backTopContent: introContent,
topContentType: isPlayingTaskVideo ? context.data.topContentType : introTypeNormalized,
introType: introTypeNormalized,
campStatus: campStatus,
currentSectionId: currentSectionId,
currentSection: currentSection, // 维护当前小节信息
campInfo: {
title: camp.title || '营地详情',
desc: camp.description || '',
cover_image: camp.cover_image || ''
},
courseList: courseList,
accessList: accessList,
hasLogged10Percent: isPlayingTaskVideo ? context.data.hasLogged10Percent : false,
sectionUnlockAtMap: {},
sectionUnlockTexts: []
};
// 如果正在播放任务视频,保留当前的 bannerVideoUrl
if (isPlayingTaskVideo && context.data.bannerVideoUrl) {
newData.bannerVideoUrl = context.data.bannerVideoUrl;
} else if (isVideo) {
newData.bannerVideoUrl = introContent;
} else {
newData.bannerVideoUrl = '';
}
// 如果正在播放任务视频,保留视频相关的其他状态
if (isPlayingTaskVideo) {
newData.currentPlayingTaskId = context.data.currentPlayingTaskId;
newData.currentPlayingTaskType = context.data.currentPlayingTaskType;
newData.videoTitle = context.data.videoTitle;
newData.currentVideoCompletionPercent = context.data.currentVideoCompletionPercent;
newData.videoCompletionMap = context.data.videoCompletionMap || {};
newData.isAutoPlay = context.data.isAutoPlay;
newData._pendingVideoPlay = context.data._pendingVideoPlay;
}
// 一次性更新所有数据
context.setData(newData);
// 标记数据已加载
context.setData({ _isDataLoaded: true });
// 生成展开状态
if (courseList.length > 0) {
generateExpandedStatus(context);
}
// 未购买的小节请求 can_unlock_section若有 unlock_at 则用于小节倒计时
if (userCamp.is_joined === true && courseList && courseList.length > 0) {
fetchUnlockAtForLockedSections(context, campId, courseList);
}
// 当小节内最后一个任务完成时,后端会在 progress/update 时自动检查并开启下一小节(若无限制且免费);
// 此处作为兜底:拉取营地数据后若发现上一节已完成、下一节未购买且可解锁免费,则再请求开启
if (userCamp.is_joined === true && courseList && courseList.length > 1) {
checkAndAutoOpenNextSections(context, campId, courseList, 1, [], silent);
}
})
.catch(function (error) {
wx.showToast({
title: error && error.message || '获取数据失败',
icon: 'none'
});
context.setData({ loading: false });
});
}