model = new \app\admin\model\data\Study(); $this->courseModel = new \app\admin\model\course\Course(); $this->userModel = new \app\admin\model\User(); $this->timeModel = new \app\admin\model\data\Time(); $this->columnBindModel = new \app\admin\model\course\ColumnBind; $this->subscriptionModel = new \app\common\model\user\Subscription(); } /** * 课程概况 * @return void */ public function total(){ $this->request->filter(['strip_tags', 'trim']); $columnId = input('column_id', 0); // 1. 获取专栏下的所有课程ID $courseIds = $this->columnBindModel ->with(['course']) ->where(['course.status' => 1, 'column_id' => $columnId,'course_id'=>['<>',0]]) ->column('course.id'); // 2. 学员总数 (student_total) // 这里我们假设 $this->subscriptionModel 是用来获取订阅/报名该专栏的用户 $studentTotal = $this->subscriptionModel->where(['course_id' => $columnId])->count(); // 3. 小节总数 (section_total) // 如果你的“小节总数”实际上是“课程数”,请使用下面这行: $totalCourseCount = count($courseIds); if (empty($courseIds)) { return $this->success("获取成功", [ 'student_total' => $studentTotal, 'section_total' => $totalCourseCount, 'participate_count' => 0, 'participate_rate' => 0.00, 'finish_count' => 0, 'finish_rate' => 0.00, ]); } // 构造基础查询条件 $baseWhere = [ 's.uniacid' => UNIACID, 's.course_id' => ['in', $courseIds], 's.column_id'=>$columnId ]; // 4. 参与人数 (participate_count) // 定义:至少学习了一个小节的去重用户数 $participateCount = $this->model ->setUniacid(false) ->alias('s') ->where($baseWhere) ->count('DISTINCT s.user_id'); // 5. 完成人数 (finish_count) // 子查询:计算每个用户完成了多少门课 // 这里的逻辑是:一个用户在一门课中只要有一个小节标记为 finish=1,就认为他完成了这门课 $finishedCoursesSubquery = $this->model ->setUniacid(false) ->alias('s') ->field('s.user_id, COUNT(DISTINCT s.course_id) as finished_course_count') ->where($baseWhere) ->where('s.finish', 1) ->group('s.user_id') ->buildSql(); // 主查询:筛选出完成课程数等于总课程数的用户 $finishCount = $this->model ->setUniacid(false) ->table("($finishedCoursesSubquery) AS user_finish_stats") ->where('user_finish_stats.finished_course_count', $totalCourseCount) ->count(); // 6. 参与率 (participate_rate) $participateRate = $studentTotal > 0 && $participateCount ? floatval(($participateCount / $studentTotal)) : 0.00; // 7. 完成率 (finish_rate) $finishRate = $studentTotal > 0 && $finishCount ? floatval(($finishCount / $studentTotal)) : 0.00; // 组织最终结果 $result = [ 'student_total' => $studentTotal, 'section_total' => $totalCourseCount, 'participate_count' => $participateCount, 'participate_rate' => round($participateRate, 2), 'finish_count' => $finishCount, 'finish_rate' => round($finishRate, 2), ]; return $this->success("获取成功", $result); } /** * 章节概括 * @return void */ public function userdetail(){ $this->request->filter(['strip_tags', 'trim']); $columnId = input('column_id/d', 0); $userId = input('user_id/d', 0); if (!$columnId || !$userId) { return $this->error("缺少必要参数"); } // 1. 获取专栏所有课程 $courseIds = $this->columnBindModel ->with(['course']) ->where(['course.status' => 1, 'column_id' => $columnId,'course_id'=>['<>',0]]) ->column('course.id'); if(!$courseIds){ return $this->success("获取成功", []); } $studyList = $this->courseModel ->alias('course') ->join($this->model->getTable()." study", "study.course_id = course.id AND study.user_id = {$userId} AND study.column_id = {$columnId} AND study.uniacid = " . UNIACID, 'LEFT') ->where([ 'course.id' => ['in', $courseIds] ]) ->field([ 'course.id as course_id', 'course.name as course_name', 'course.cover as course_cover', 'course.type as course_type', 'COUNT(study.id) AS record_count', 'SUM(study.total_time) AS total_time_sec', 'MAX(study.media_progress) AS media_progress_sec', 'MIN(study.start_time) AS first_study_time', 'MAX(study.end_time) AS last_study_time', 'SUM(CASE WHEN study.finish = 1 THEN 1 ELSE 0 END) AS finish_count' ]) ->group('course.id') ->order('total_time_sec','desc') ->select(); foreach ($studyList as $cid => $row) { $row['course'] = [ 'id' => $row['course_id'], 'name' => $row['course_name'], 'cover' => $row['course_cover'], 'type' => $row['course_type'], ]; // 参与状态 if ($row['finish_count'] > 0) { $row['status'] = '已完成'; } elseif ($row['record_count'] > 0) { $row['status'] = '进行中'; } // 时长(秒→分钟) $row['total_minutes'] = intval($row['total_time_sec']); // 观看进度 $row['progress_minutes'] = intval($row['media_progress_sec']); // 时间格式 $row['first_study_time'] = $row['first_study_time'] ? date('Y-m-d H:i:s', $row['first_study_time']) : '-'; $row['last_study_time'] = $row['last_study_time'] ? date('Y-m-d H:i:s', $row['last_study_time']) : '-'; $result[] = $row; } return $this->success("获取成功", $result); } /** * 章节概括 * @return void */ public function course(){ $this->request->filter(['strip_tags','trim']); $columnId = input('column_id/d', 0); if (!$columnId) { return $this->error("缺少必要参数"); } // 1. 获取专栏内所有课程 $courses = $this->courseModel ->alias('c') ->join($this->columnBindModel->getTable()." cb", "cb.course_id = c.id AND cb.column_id = {$columnId}", 'LEFT') ->where(['c.status' => 1,'cb.course_id'=>['<>',0]]) ->field('c.id,c.name,c.cover,c.type') ->group('cb.course_id') ->select(); if (!$courses) { return $this->success("获取成功", ['total' => 0, 'rows' => []]); } $courseIds = array_column($courses, 'id'); // 2. 统计每门课程参与人数和完成人数 $studyStats = $this->model ->alias('s') ->where(['s.column_id' => $columnId, 's.uniacid' => UNIACID]) ->whereIn('s.course_id', $courseIds) ->field([ 's.course_id', 'COUNT(DISTINCT s.user_id) AS participate_user_count', 'SUM(CASE WHEN s.finish = 1 THEN 1 ELSE 0 END) AS finish_user_count' ]) ->group('s.course_id') ->select(); // 3. 建立课程ID映射 $statsMap = []; foreach ($studyStats as $stat) { $statsMap[$stat['course_id']] = $stat; } // 4. 计算专栏总用户数(用于参与率) $totalUserCount = $this->subscriptionModel ->where(['course_id' => $columnId]) ->count(); // 5. 构造结果 $result = []; foreach ($courses as $course) { $cid = $course['id']; $participate = isset($statsMap[$cid]) ? (int)$statsMap[$cid]['participate_user_count'] : 0; $finish = isset($statsMap[$cid]) ? (int)$statsMap[$cid]['finish_user_count'] : 0; $result[] = [ 'course' => [ 'id' => $course['id'], 'name' => $course['name'], 'cover' => $course['cover'], 'type' => $course['type'], ], 'participate_user_count' => $participate, 'participate_rate' => $totalUserCount > 0 && $participate ? number_format($participate / $totalUserCount * 100, 2) . '%' : '0%', 'finish_user_count' => $finish, 'finish_rate' => $participate > 0 && $totalUserCount ? number_format($finish / $totalUserCount * 100, 2) . '%' : '0%', ]; } return $this->success("获取成功", ['total' => count($result), 'rows' => $result]); } protected $exportCourseIndex = 0; public function courseDownload(){ $this->request->filter(['strip_tags','trim']); $columnId = input('column_id/d', 0); if (!$columnId) { return $this->error("缺少必要参数"); } // 1. 获取专栏内所有课程 $courses = $this->courseModel ->alias('c') ->join($this->columnBindModel->getTable()." cb", "cb.course_id = c.id AND cb.column_id = {$columnId}", 'LEFT') ->where(['c.status' => 1,'cb.course_id'=>['<>',0]]) ->field('c.id,c.name') ->group('cb.course_id') ->select(); if (!$courses) { return $this->error("未获取到相关课程"); } $courseIds = array_column($courses, 'id'); // 2. 统计每门课程参与人数和完成人数 $studyStats = $this->model ->alias('s') ->where(['s.column_id' => $columnId, 's.uniacid' => UNIACID]) ->whereIn('s.course_id', $courseIds) ->field([ 's.course_id', 'COUNT(DISTINCT s.user_id) AS participate_user_count', 'SUM(CASE WHEN s.finish = 1 THEN 1 ELSE 0 END) AS finish_user_count' ]) ->group('s.course_id') ->select(); // 3. 建立课程ID映射 $statsMap = []; foreach ($studyStats as $stat) { $statsMap[$stat['course_id']] = $stat; } // 4. 计算专栏总用户数(用于参与率) $totalUserCount = $this->subscriptionModel ->where(['course_id' => $columnId]) ->count(); $excelTitle = '小节概况【'.date('Y-m-d h:i',time()).'】'; $objPHPExcel = new \PHPExcel(); //设置Excel属性 $objPHPExcel->getProperties() ->setTitle($excelTitle); //设置标题 $objPHPExcel->getActiveSheet()->setCellValue('A1', '课程');//可以指定位置 $objPHPExcel->getActiveSheet()->setCellValue('B1', '参与人数');//可以指定位置 $objPHPExcel->getActiveSheet()->setCellValue('C1', '参与率'); $objPHPExcel->getActiveSheet()->setCellValue('D1', '完成人数'); $objPHPExcel->getActiveSheet()->setCellValue('E1', '完成率'); foreach ($courses as $course) { $cid = $course['id']; $participate = isset($statsMap[$cid]) ? (int)$statsMap[$cid]['participate_user_count'] : 0; $finish = isset($statsMap[$cid]) ? (int)$statsMap[$cid]['finish_user_count'] : 0; $objPHPExcel->getActiveSheet()->setCellValue('A'.(2 + $this->exportCourseIndex), $course['name']); $objPHPExcel->getActiveSheet()->setCellValue('B'.(2 + $this->exportCourseIndex), $participate); $objPHPExcel->getActiveSheet()->setCellValue('C'.(2 + $this->exportCourseIndex), ($totalUserCount > 0 && $participate? number_format($participate / $totalUserCount * 100, 2) . '%' : '0%')); $objPHPExcel->getActiveSheet()->setCellValue('D'.(2 + $this->exportCourseIndex), $finish); $objPHPExcel->getActiveSheet()->setCellValue('E'.(2 + $this->exportCourseIndex), ($participate > 0 && $finish ? number_format($finish / $totalUserCount * 100, 2) . '%' : '0%')); $this->exportCourseIndex++; } //设置字体 $objPHPExcel->getActiveSheet()->getStyle('A1:E1')->getFont()->setBold(true)->getColor()->setARGB(\PHPExcel_Style_Color::COLOR_BLACK); //设置水平居中 $objPHPExcel->getActiveSheet()->getStyle('A1:E1')->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER); //设置背景颜色 $objPHPExcel->getActiveSheet()->getStyle('A1:E1')->getFill()->setFillType(\PHPExcel_Style_Fill::FILL_SOLID)->getStartColor()->setARGB('D9FFC125'); $objPHPExcel->getActiveSheet()->getColumnDimension('A')->setWidth(20); $objPHPExcel->getActiveSheet()->getColumnDimension('B')->setWidth(15); $objPHPExcel->getActiveSheet()->getColumnDimension('C')->setWidth(15); $objPHPExcel->getActiveSheet()->getColumnDimension('D')->setWidth(18); $objPHPExcel->getActiveSheet()->getColumnDimension('E')->setWidth(18); //激活当前表 $objPHPExcel->setActiveSheetIndex(0); ob_end_clean();//清除缓冲区,避免乱码 //弹出提示下载文件 header('pragma:public'); header("Content-Disposition:attachment;filename={$excelTitle}.xlsx"); header('Cache-Control: max-age=0'); $objWriter = PHPExcel_IOFactory:: createWriter($objPHPExcel, 'Excel2007'); $objWriter->save( 'php://output'); } /** * 学员详情(包含未参与学习的用户) */ public function user() { $this->request->filter(['strip_tags', 'trim']); $columnId = input('column_id/d', 0); if (!$columnId) { return $this->error("缺少必要参数"); } list($where, $sort, $order, $offset, $limit) = $this->buildparams(); // 1. 复用工具方法:获取订阅学员列表(带分页) $subscriptionResult = $this->getSubscriptionUsers($columnId, $limit); $userList = $subscriptionResult['userList']; $pagination = $subscriptionResult['pagination']; if (empty($userList)) { return $this->success("获取成功", [ "total" => 0, "rows" => [], "total_section_count" => 0 ]); } // 2. 复用工具方法:获取学员学习统计数据 $statResult = $this->getUserStudyStats($columnId, $userList); $map = $statResult['map']; $hasStudyUserIds = $statResult['hasStudyUserIds']; $totalSectionCount = $statResult['totalSectionCount']; // 3. 复用工具方法:组装学员详情数据 $rows = $this->assembleUserDetailData($userList, $map, $hasStudyUserIds, $totalSectionCount); // 4. 自定义排序(当前方法特有逻辑) usort($rows, function($a, $b) use ($sort, $order) { if ($a['sort_flag'] != $b['sort_flag']) { return $b['sort_flag'] - $a['sort_flag']; } if (empty($sort)) { return 0; } $fieldA = $a[$sort] ?? 0; $fieldB = $b[$sort] ?? 0; if (in_array($sort, ['first_study_time', 'last_study_time'])) { $fieldA = $fieldA != '-' ? strtotime($fieldA) : 0; $fieldB = $fieldB != '-' ? strtotime($fieldB) : 0; } return $order == 'desc' ? $fieldB - $fieldA : $fieldA - $fieldB; }); return $this->success("获取成功", [ "total" => $pagination->total(), "rows" => $rows, "total_section_count" => $totalSectionCount ]); } /** * 学员详情导出 */ public function userDownload() { $this->request->filter(['strip_tags', 'trim']); $columnId = input('column_id/d', 0); if (!$columnId) { return $this->error("缺少必要参数:column_id"); } // 1. 复用工具方法:获取订阅学员列表(无分页) $subscriptionResult = $this->getSubscriptionUsers($columnId, 0); $userList = $subscriptionResult['userList']; if (empty($userList)) { return $this->error("无学员数据可导出"); } // 2. 复用工具方法:获取学员学习统计数据 $statResult = $this->getUserStudyStats($columnId, $userList); $map = $statResult['map']; $totalSectionCount = $statResult['totalSectionCount']; // 3. 组装导出数据(当前方法特有逻辑) $exportData = $this->assembleExportData($userList, $map, $totalSectionCount); // 4. 排序(导出特有排序:participated_count降序) usort($exportData, function($a, $b) { return $b['participated_count'] - $a['participated_count']; }); // 5. 执行导出(Excel逻辑不变) $this->exportExcel($exportData); } /** * 工具方法:获取专栏订阅学员列表 * @param int $columnId 专栏ID * @param int $limit 分页条数(0表示无分页) * @return array ['userList' => 学员列表, 'pagination' => 分页对象(无分页时为null)] */ private function getSubscriptionUsers(int $columnId, int $limit = 0) { $result = [ 'userList' => [], 'pagination' => null ]; // 构建查询 $query = $this->subscriptionModel ->with(['user', 'mobileuser']) ->distinct(true) ->where(['course_id' => $columnId]) ->group('subscription.id'); // 分页/无分页处理 if ($limit > 0) { $pagination = $query->paginate($limit); $result['pagination'] = $pagination; $list = $pagination->items(); } else { $list = $query->select(); } // 整理学员数据(user/mobileuser统一格式) if ($list) { foreach ($list as &$row) { if ($row->user->id) { $row->getRelation('user')->visible(\app\admin\model\User::$listShowField); $row->user->user_id = $row->user->id; $result['userList'][] = $row->user; continue; } if ($row->mobileuser->id) { $row->getRelation('mobileuser')->visible(\app\admin\model\User::$listShowField); $row->mobileuser->user_id = $row->mobileuser->id; $result['userList'][] = $row->mobileuser; continue; } } } return $result; } /** * 工具方法:获取学员学习统计数据 * @param int $columnId 专栏ID * @param array $userList 学员列表(含user_id) * @return array ['map' => 学习数据映射, 'hasStudyUserIds' => 有学习记录的用户ID, 'totalSectionCount' => 专栏总课程数] */ private function getUserStudyStats(int $columnId, array $userList) { $userIds = array_column($userList, "user_id"); // 统计学习数据 $studyStats = $this->model ->alias("s") ->where([ "s.column_id" => $columnId, "s.uniacid" => UNIACID ]) ->whereIn("s.user_id", $userIds) ->field([ 's.user_id', 'COUNT(s.id) AS participated_count', 'SUM(CASE WHEN s.finish = 1 THEN 1 ELSE 0 END) AS finished_count', 'SUM(s.total_time) AS total_study_time_minutes', 'MIN(s.start_time) AS first_study_time', 'MAX(s.end_time) AS last_study_time', ]) ->group("s.user_id") ->select(); // 构建映射和有学习记录的用户ID列表 $map = []; $hasStudyUserIds = []; foreach ($studyStats as $row) { $map[$row['user_id']] = $row; $hasStudyUserIds[] = $row['user_id']; } // 获取专栏总课程数 $courseIds = $this->columnBindModel ->where(['status' => 1, 'column_id' => $columnId]) ->column('course_id'); $totalSectionCount = count($courseIds); return [ 'map' => $map, 'hasStudyUserIds' => $hasStudyUserIds, 'totalSectionCount' => $totalSectionCount ]; } /** * 工具方法:组装学员详情页数据(带排序标识) * @param array $userList 学员列表 * @param array $map 学习数据映射 * @param array $hasStudyUserIds 有学习记录的用户ID * @param int $totalSectionCount 专栏总课程数 * @return array 组装后的详情数据 */ private function assembleUserDetailData(array $userList, array $map, array $hasStudyUserIds, int $totalSectionCount) { $rows = []; foreach ($userList as $user) { $uid = $user["user_id"]; $stat = $map[$uid] ?? [ "participated_count" => 0, "finished_count" => 0, "first_study_time" => null, "last_study_time" => null, "total_study_time_minutes" => 0, ]; $participated = intval($stat["participated_count"]); $finished = intval($stat["finished_count"]); $rows[] = [ "sort_flag" => in_array($uid, $hasStudyUserIds) ? 1 : 0, "user" => [ "id" => $uid, "username" => $user["username"], "nickname" => $user["nickname"], "avatar" => $user["avatar"], ], "participated_count" => $participated, "finished_count" => $finished, "participated_rate" => $totalSectionCount && $participated ? number_format($participated / $totalSectionCount * 100, 2) . "%" : "0%", "completion_rate" => $totalSectionCount && $finished ? number_format($finished / $totalSectionCount * 100, 2) . "%" : "0%", "study_status" => $participated == 0 ? "未参与" : ($finished >= $totalSectionCount ? "已完成" : "进行中"), "first_study_time" => $stat["first_study_time"] ? date("Y-m-d H:i:s", $stat["first_study_time"]) : "-", "last_study_time" => $stat["last_study_time"] ? date("Y-m-d H:i:s", $stat["last_study_time"]) : "-", "total_study_time_minutes" => intval($stat["total_study_time_minutes"]), ]; } return $rows; } /** * 工具方法:组装导出数据 * @param array $userList 学员列表 * @param array $map 学习数据映射 * @param int $totalSectionCount 专栏总课程数 * @return array 导出格式数据 */ private function assembleExportData(array $userList, array $map, int $totalSectionCount) { $exportData = []; foreach ($userList as $user) { $uid = $user["user_id"]; $stat = $map[$uid] ?? [ "participated_count" => 0, "finished_count" => 0, "first_study_time" => null, "last_study_time" => null, "total_study_time_minutes" => 0, ]; $participated = intval($stat["participated_count"]); $finished = intval($stat["finished_count"]); $exportData[] = [ 'username' => $user["username"] ?? "未知用户", 'study_status' => $participated == 0 ? "未参与" : ($finished >= $totalSectionCount ? "已完成" : "进行中"), 'participated_count' => $participated, 'participated_rate' => $totalSectionCount && $participated ? number_format($participated / $totalSectionCount * 100, 2) . "%" : "0%", 'finished_count' => $finished, 'completion_rate' => $totalSectionCount && $finished ? number_format($finished / $totalSectionCount * 100, 2) . "%" : "0%", 'first_study_time' => $stat["first_study_time"] ? date("Y-m-d H:i:s", $stat["first_study_time"]) : "-", 'last_study_time' => $stat["last_study_time"] ? date("Y-m-d H:i:s", $stat["last_study_time"]) : "-", 'total_study_time' => number_format((float)$stat["total_study_time_minutes"] / 60, 1) . " 分钟", ]; } return $exportData; } /** * 工具方法:Excel导出执行 * @param array $exportData 导出数据 */ private function exportExcel(array $exportData) { $excelTitle = '学员详情【' . date('Y-m-d H:i', time()) . '】'; $objPHPExcel = new \PHPExcel(); // 设置Excel属性和表头 $objPHPExcel->getProperties()->setTitle($excelTitle); $headers = [ 'A1' => '学员用户名', 'B1' => '课程参与状态', 'C1' => '小节参与数', 'D1' => '小节参与率', 'E1' => '小节完成数', 'F1' => '小节完成率', 'G1' => '首次学习时间', 'H1' => '最后学习时间', 'I1' => '学习时长(分钟)' ]; foreach ($headers as $cell => $title) { $objPHPExcel->getActiveSheet()->setCellValue($cell, $title); } // 填充数据 $startRow = 2; foreach ($exportData as $data) { $objPHPExcel->getActiveSheet() ->setCellValue('A' . $startRow, $data['username']) ->setCellValue('B' . $startRow, $data['study_status']) ->setCellValue('C' . $startRow, $data['participated_count']) ->setCellValue('D' . $startRow, $data['participated_rate']) ->setCellValue('E' . $startRow, $data['finished_count']) ->setCellValue('F' . $startRow, $data['completion_rate']) ->setCellValue('G' . $startRow, $data['first_study_time']) ->setCellValue('H' . $startRow, $data['last_study_time']) ->setCellValue('I' . $startRow, $data['total_study_time']); $startRow++; } // 设置样式 $objPHPExcel->getActiveSheet()->getStyle('A1:I1')->getFont()->setBold(true)->getColor()->setARGB(\PHPExcel_Style_Color::COLOR_BLACK); $objPHPExcel->getActiveSheet()->getStyle('A1:I1')->getAlignment()->setHorizontal(\PHPExcel_Style_Alignment::HORIZONTAL_CENTER); $objPHPExcel->getActiveSheet()->getStyle('A1:I1')->getFill()->setFillType(\PHPExcel_Style_Fill::FILL_SOLID)->getStartColor()->setARGB('D9FFC125'); $columnWidths = ['A' => 20, 'B' => 15, 'C' => 15, 'D' => 18, 'E' => 18, 'F' => 18, 'G' => 20, 'H' => 20, 'I' => 16]; foreach ($columnWidths as $column => $width) { $objPHPExcel->getActiveSheet()->getColumnDimension($column)->setWidth($width); } // 导出响应 $objPHPExcel->setActiveSheetIndex(0); ob_end_clean(); header('pragma:public'); header("Content-Disposition:attachment;filename={$excelTitle}.xlsx"); header('Cache-Control: max-age=0'); $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007'); $objWriter->save('php://output'); } // 原有的导出索引属性(当前已无需使用,可删除) // protected $exportUserIndex = 0; }