function ($query) { $query->withField('id, name'); } ]) ->where('status', CommonStatus::NORMAL) ->where('is_only_room', 0)// 过滤仅考场使用的试卷 ->whereRaw("((start_time = 0 and end_time = 0) or (start_time < {$now} and end_time > {$now}))"); // 分类 if ($cate_id) { $child_cate_ids = CateModel::getChildId($cate_id); array_push($child_cate_ids, $cate_id); $query->whereIn('cate_id', $child_cate_ids); } // 排序 if ($sort && $sort != 'null') { $sort = explode('|', $sort); $field = $sort[0]; $order_by = $sort[1]; $field = in_array($field, ['join_count']) ? $field : 'join_count'; $order_by = $order_by == 'desc' ? 'desc' : 'asc'; $query->order("{$field} $order_by"); } $list = $query->paginate(); $this->success('', ['list' => $list]); } public function lists() { $list = Db::name('exam_paper')->field('id,image,title,updatetime')->where('status','normal') ->order('weigh desc, id desc')->autopage()->select(); $list = list_domain_image($list,['image']); $this->success('', $list); } public function detail(){ $id = input('id'); $info = Db::name('exam_paper')->field('id,image,title,content,updatetime,quantity')->where('status','normal') ->find($id); $info = info_domain_image($info,['image']); $this->success('', $info); } /** * 试卷取题接口 */ public function getExamQuestion() { $paper_id = input('paper_id/d', 0); $room_id = input('room_id/d', 0); // 验证是否需要绑定手机号 // UserModel::isMustBindMobile($this->auth->getUser()); // 预创建考场考试记录 // $room_grade_id = ExamService::preRoomGrade($room_id, $this->auth->id); // 获取试卷题目 $question_data = ExamService::getExamQuestion($paper_id, $room_id); // $this->success('', array_merge($question_data, ['room_grade_id' => $room_grade_id])); $this->success('', $question_data); } //开始考试接口 public function startpaper(){ $paper_id = input('paper_id/d', 0); $user_id = $this->auth->id; //检查考试状态 /*$check = Db::name('exam_grade')->where('user_id', $user_id)->where('status',1)->find(); if($check){ $this->success('您有其他考试正在进行中,即将继续考试',0);//直接给成功,数据返回0,前端跳转 }*/ //检查试卷 $paper = PaperModel::get($paper_id); switch (true) { case !$paper: $this->error('科普答题信息不存在'); case $paper->status != 'NORMAL': $this->error('科普答题信息不存在'); case $paper->mode == 'RANDOM' && !$paper->configs: $this->error('科普答题信息未配置'); } //时间限制 /*if ($paper['start_time'] > 0 && $paper['start_time'] > time()) { $this->error('该试卷未开始,不能参与考试'); } if ($paper['end_time'] > 0 && $paper['end_time'] < time()) { $this->error('该试卷已结束,不能参与考试'); }*/ //考试资格 /*if(!in_array($user_id,explode(',',$paper['user_ids']))){ $this->error('您不能参加该考试'); }*/ //次数限制 if ($paper['limit_count'] > 0){ $my_count = Db::name('exam_grade')->where('user_id', $user_id)->where('paper_id', $paper_id)->where('status',2)->count(); if($my_count >= $paper['limit_count']) { $this->error('在该科普中您的答题次数已达上限'); } } //记录为已开始,计划任务倒计时之后 自动结束 $data = [ 'cate_id' => $paper['cate_id'], 'user_id' => $this->auth->id, 'paper_id' => $paper_id, 'mode' => $paper['mode'], 'total_score' => $paper['total_score'], 'total_count' => $paper['quantity'], 'start_time' => time(), 'createtime' => time(), 'status' => 1, 'limit_time' => $paper['limit_time'], //限时N秒 'last_time' => $paper['limit_time'] > 0 ? (time() + $paper['limit_time']) : 0, //最后限制交卷时间,时间戳 ]; $grade_id = Db::name('exam_grade')->insertGetId($data); $this->success('',$grade_id); } //进行中考试 public function half_examing(){ $user_id = $this->auth->id; $check = Db::name('exam_grade')->where('user_id', $user_id)->where('status',1)->find(); if(empty($check)){ $this->error('您没有进行中的考试'); } $paper_id = $check['paper_id']; // 获取试卷题目 $question_data = ExamService::getExamQuestion($paper_id, 0); $question_data['paper']['limit_time'] = $check['last_time'] - time();//倒计时秒数 $this->success('', $question_data); } /** * 交卷 */ public function submit() { $request = Request::instance(); $user_id = $this->auth->id; $grade_id = $request->post('grade_id'); $paper_id = $request->post('paper_id/d', 0); $questions = $request->post('questions/a', []); $start_time = $request->post('start_time/d', time()); $room_id = 0; $room_grade_id = $request->post('room_grade_id/d', 0); if (!$user_id || !$paper_id || !$questions) { $this->error('提交数据有误'); } $check = Db::name('exam_grade')->where('status',1)->where('id',$grade_id)->where('user_id',$user_id)->where('paper_id',$paper_id)->find(); if(!$check){ $this->error('交卷有误,或者您已交卷'); } $start_time = $check['start_time']; // 考场考试 if ($room_id) { if (!$room_grade_id) { $this->error('提交数据不合法'); } // 考场考试 $result = ExamService::roomExam($user_id, $room_id, $room_grade_id, $questions, $start_time, $paper, $room, $is_makeup, $room_grade_log); // 记录考场考试成绩 $room_grade_log->allowField(true)->save( array_merge( $result, [ // 'cate_id' => $paper['cate_id'], 'user_id' => $user_id, 'paper_id' => $paper_id, 'is_makeup' => $is_makeup, 'is_pre' => 0, // 提交成绩后不再为预创建标记 ], [ 'exam_mode' => ExamMode::ROOM, ] ) ); } else { $result = ExamService::paperExam($user_id, $paper_id, $questions, $start_time, $paper); // 记录考试成绩 /*GradeModel::create(array_merge( $result, [ 'cate_id' => $paper['cate_id'], 'user_id' => $user_id, 'paper_id' => $paper_id, ], [ // 'exam_mode' => ExamMode::PAPER, 'date' => date('Y-m-d'), ]), true);*/ $update = array_merge( $result, [ 'updatetime' => time(), 'date' => date('Y-m-d'), 'status' => 2, 'finish_time' => time(), ]); unset($update['pass_score']); unset($update['start_time']); $rs = Db::name('exam_grade')->where('id',$grade_id)->update($update); if($rs === false){ $this->error('交卷失败'); } // 异步推送积分到工行(写入队列) $cephone = '15388010006'; //$cephone = $this->auth->mobile; $this->addIcbcQueue($cephone, $result['score'], '10938', $this->auth->nickname); } $result['nickname'] = $this->auth->nickname; unset($result['question_ids']); unset($result['user_answers']); unset($result['configs']); $this->success('',$result); } /** * 积分维护接口 - 推送积分数据到工行 * @param string $mobile_phone 用户手机号 * @param int $integral_value 变动的积分值(整数) * @param string $integral_type 积分类型 * @param array $remarks 备用字段数组(可选,最多7个) * @return array 返回接口响应结果 */ public function update_villager_integral($mobile_phone = '', $integral_value = 0, $integral_type = '', $remarks = []) { // PHP 8+ 兼容:确保参数不为null $mobile_phone = $mobile_phone ?? ''; $integral_value = $integral_value ?? 0; $integral_type = $integral_type ?? ''; $remarks = $remarks ?? []; // 如果没有传入手机号,尝试从登录用户获取 if (empty($mobile_phone) && isset($this->auth->mobile)) { $mobile_phone = $this->auth->mobile; } // 生成16位唯一序列号(时间戳10位+随机数6位) $fSeqNo = time() . str_pad(mt_rand(0, 999999), 6, '0', STR_PAD_LEFT); // 构建业务参数 $biz_content = [ 'fSeqNo' => $fSeqNo, 'corpCode' => 'xingfulishequ', 'mobilePhone' => $mobile_phone, 'integralValue' => (string)$integral_value, // 确保是字符串类型 'integralType' => $integral_type, ]; // 添加备用字段(如果有) for ($i = 1; $i <= 7; $i++) { if (isset($remarks[$i - 1]) && !empty($remarks[$i - 1])) { $biz_content['remark' . $i] = $remarks[$i - 1]; } } // 工行RSA密钥(字符串格式) $public_key = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwFgHD4kzEVPdOj03ctKM7KV+16bWZ5BMNgvEeuEQwfQYkRVwI9HFOGkwNTMn5hiJXHnlXYCX+zp5r6R52MY0O7BsTCLT7aHaxsANsvI9ABGx3OaTVlPB59M6GPbJh0uXvio0m1r/lTW3Z60RU6Q3oid/rNhP3CiNgg0W6O3AGqwIDAQAB'; $private_key = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCR8/ZvKPAdZzsyvapySvztQm56s1N59ynKMOWpUbK4c5MNWMl+q3dvsp+UiSAx2TAeGkYfW3W6nO/3Y0hAZki99fmuzpPujEeHhs79HNwGZYQjN71Vck2JeflTq8jpL+9/up0Kz2nbwtZDMKTTEgbfNeO24gV1bmvD2kQ9j66RIyuXSDwQbVbQfl6LiqKoJm3rbtsbwX1Ytc0/Szslyor1VdPZWnHDMm3m78Hqu7X3vL6K7fAW/4FVbeKV7vWjvyiTQfETmuADqMdsqV5YeqUZWE/Dnzg+6JV/3L9nJ8f+/mPlh8k1h1oW035GrADKFKf1M2ujKKeHICnj7qKCdBeNAgMBAAECggEAHkh+U2YtHAy1Tbvox7ojbJ8iCTd4FJBiDV/D5zPaX0crtdM8S5oMOBLZ5ZnmIjGsODK/ZfY2ITg62huxfBs88J0+5zRZoV9d4BLqk74PMQyTNDN2h2omCGZUgzXbg/a8PMZdm0aZ8k0k4+AN8vWEk3+89c9Dzq/QkFyTWCqdz+Mp9NkcOjA255kj5/D1q9Zj0x9VcmKg3oTtrKL/dkspUxVaMKXcpo6J4AszC05tT3N0SNLhcq51I6B9QdbYsYCkP9whPNYIl/y4dN4QnNEivQzA5/ltr5DDQZc7Xke1+SpJN+ylBOBJ8yvNoDnuxx0xEWuNJ5bUzyri/DNjZRKNQQKBgQDKkxn5R4Q3rChI/KVIXHMecTs9fXN5pbnA8UKU6ZeTl+wHffxwRieHZJJp1nDcQ5i8YXCtqEJcJGFn3T0rEjVCJ/RVHclFO+TjYaK9HtyNJLPrvKmAzjCzV1yPfe4qmpgJddzZI7Vii2uFgklixvhkoY74hwSJtvLPTO0p0xrYdQKBgQC4cgfpCMLYlaHQGz+dAEf4IewQpwBGn7XShCpiXGFciGZZEIzvJMzXP7yo8pDMHQbB4kQsTRRG2fcdHGWI5VQGHPqG5O0tUueKGUlSg7j8Y/Pp8ZrBGSrlilAf17I/u9MC7Xe2ZRVGNgPDYAyjMEGmClI0n2+aN6b4CFVBjYmfuQKBgEKO9KDIE7QrF41rnW7aGWTuNVWty2wzvIWdf4/n9EqlRwLrLS9CjahZrhWiRLDKcPusVFZqi2s09OAoe/mT4PXcpNX2lHPwCvN+1/allje10HvrIBJXLP8v/BSVftR2uO+azzZ1GhrHzksulKgk0eZWguA7lI0fFEZycxYj65UlAoGAD9p1RZlkLfuGgf2llRgOF4zK3o+MHYXiuep0PioUkECFE4ixpGh0Vtf6nkbjHTgteYK6O1iQsppPfCgRrheQBkp9WhTZMfkbP6p2u+nof4ET2PrUQ16naj1eL655erLpKypADORZVMSVxDhAPdKLAfuHH1DI5ed8qXsF4PGKb7kCgYEAm/d+daT6YsbHDZlJ/J9Q8rRkKmiqj43NGQSHKg6Z6BEDibm8wRmj3Itu1N6XVChuaH+ekJzvUnZ/q1nyYzGvy6bOHYn3ziF9aH7wuhcRZ4qARmKDnzTBLg2QXBK1+400O3LJ+sAH/yuH/Y4hzRE6YMxBQpdYfnlJcIFVimAsT1w='; // 构建通用请求参数 $data = [ 'app_id' => '10000000000004096993', 'msg_id' => createUniqueNo('msg', time()), 'format' => 'json', 'charset' => 'UTF-8', 'encrypt_type' => 'AES', 'sign_type' => 'RSA2', ]; // 记录请求日志(调试用) \think\Log::record('积分维护接口请求参数: ' . json_encode([ 'app_id' => $data['app_id'], 'msg_id' => $data['msg_id'], 'biz_content' => $biz_content ], JSON_UNESCAPED_UNICODE), 'info'); try { // 检查必要的类和函数 if (!class_exists('\DefaultIcbcClient')) { \think\Log::record('DefaultIcbcClient 类不存在', 'error'); return [ 'return_code' => '-98', 'return_msg' => 'DefaultIcbcClient 类不存在', 'raw_response' => '' ]; } if (!function_exists('createUniqueNo')) { \think\Log::record('createUniqueNo 函数不存在', 'error'); return [ 'return_code' => '-97', 'return_msg' => 'createUniqueNo 函数不存在', 'raw_response' => '' ]; } // 创建工行客户端 \think\Log::record('准备创建 DefaultIcbcClient...', 'info'); $client = new \DefaultIcbcClient( $data['app_id'], $private_key, $data['sign_type'], $data['charset'], $data['format'], $public_key, '', '', '', '' ); \think\Log::record('DefaultIcbcClient 创建成功', 'info'); // 构建请求参数 $request = [ 'serviceUrl' => 'https://gw.dccnet.com.cn:8084/api/mybank/farm/farmplatf/updateVillagerIntegral/V1', 'method' => 'POST', 'isNeedEncrypt' => false, 'extraParams' => null, 'biz_content' => $biz_content, ]; // 发送请求 $response = $client->execute($request, $data['msg_id'], ''); // 记录原始响应(调试用) \think\Log::record('积分维护接口原始响应长度: ' . strlen($response), 'info'); // 检查响应是否为空 if (empty($response)) { $resultData = [ 'return_code' => '-1', 'return_msg' => '接口无响应', 'raw_response' => $response ]; $this->writeIcbcLog(false, $resultData, $biz_content); return $resultData; } // 清理响应内容:提取 JSON 部分 // 工行接口可能返回调试信息,需要清理 $clean_response = $this->cleanResponse($response); // 如果清理后的响应与原始不同,记录 if ($clean_response !== $response) { \think\Log::record('积分维护接口响应包含非JSON内容,已清理', 'info'); } // 解析响应 $result = json_decode($clean_response, true); // 检查JSON解析是否成功 if (json_last_error() !== JSON_ERROR_NONE) { \think\Log::record('积分维护接口JSON解析失败: ' . json_last_error_msg(), 'error'); $resultData = [ 'return_code' => '-2', 'return_msg' => 'JSON解析失败: ' . json_last_error_msg(), 'raw_response' => $response, 'clean_response' => $clean_response ]; $this->writeIcbcLog(false, $resultData, $biz_content); return $resultData; } // 检查是否有response_biz_content(按API文档,响应数据在这个字段里) if (isset($result['response_biz_content'])) { $biz_result = json_decode($result['response_biz_content'], true); if ($biz_result) { // 合并业务响应和原始响应 $resultData = array_merge($biz_result, ['raw_response' => $response]); $this->writeIcbcLog($resultData['return_code'] == 0, $resultData, $biz_content); return $resultData; } } // 如果直接有return_code,直接返回 if (isset($result['return_code'])) { $this->writeIcbcLog($result['return_code'] == 0, $result, $biz_content); return $result; } // 返回完整结果 $resultData = $result ? $result : [ 'return_code' => '-3', 'return_msg' => '未知响应格式', 'raw_response' => $response ]; $this->writeIcbcLog(false, $resultData, $biz_content); return $resultData; } catch (\Exception $e) { // 捕获异常 \think\Log::record('积分维护接口异常: ' . $e->getMessage(), 'error'); $resultData = [ 'return_code' => '-99', 'return_msg' => '接口调用异常: ' . $e->getMessage(), 'raw_response' => '' ]; $this->writeIcbcLog(false, $resultData, $biz_content); return $resultData; } } /** * 添加工行积分任务到队列 * @param string $mobile_phone 手机号 * @param int $integral_value 积分值 * @param string $integral_type 积分类型 * @param string $nickname 用户昵称 */ private function addIcbcQueue($mobile_phone, $integral_value, $integral_type, $nickname) { try { // PHP 8+ 兼容:确保所有值都不为null $data = [ 'mobile_phone' => $mobile_phone ?? '', 'integral_value' => $integral_value ?? 0, 'integral_type' => $integral_type ?? '', 'nickname' => $nickname ?? '', 'status' => 0, // 0-待处理, 1-处理中, 2-成功, 3-失败 'retry_count' => 0, 'createtime' => time(), 'updatetime' => time(), ]; Db::name('icbc_queue')->insert($data); // 不立即处理,完全由定时任务处理 // 这样可以避免 fastcgi_finish_request() 影响正常响应 } catch (\Exception $e) { // 队列写入失败,记录日志但不影响主流程 \think\Log::record('工行队列写入失败: ' . $e->getMessage(), 'error'); } } /** * 异步处理工行队列(不阻塞当前请求) */ private function processIcbcQueueAsync() { // 使用fastcgi_finish_request()立即返回响应给用户 if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } // 后台处理队列 $this->processIcbcQueue(); } /** * 处理工行队列任务 */ public function processIcbcQueue() { // 获取待处理的任务(限制每次处理10条) $tasks = Db::name('icbc_queue') ->where('status', 0) ->where('retry_count', '<', 3) // 最多重试3次 ->limit(10) ->select(); foreach ($tasks as $task) { try { // 更新状态为处理中 Db::name('icbc_queue')->where('id', $task['id'])->update([ 'status' => 1, 'updatetime' => time() ]); // 调用工行接口 $result = $this->update_villager_integral( $task['mobile_phone'], $task['integral_value'], $task['integral_type'], [$task['nickname']] ); // 先检查返回值是否为数组 if (!is_array($result)) { \think\Log::record('工行接口返回值异常[ID:'.$task['id'].']: ' . var_export($result, true), 'error'); // 返回值异常,标记为失败 $retry_count = $task['retry_count'] + 1; $status = $retry_count >= 3 ? 3 : 0; Db::name('icbc_queue')->where('id', $task['id'])->update([ 'status' => $status, 'retry_count' => $retry_count, 'result' => json_encode(['error' => '返回值不是数组', 'raw' => var_export($result, true)], JSON_UNESCAPED_UNICODE), 'error_msg' => '接口返回值格式错误', 'updatetime' => time() ]); continue; } // 根据结果更新状态 if (isset($result['return_code']) && $result['return_code'] == 0) { // 成功 Db::name('icbc_queue')->where('id', $task['id'])->update([ 'status' => 2, 'result' => json_encode($result, JSON_UNESCAPED_UNICODE), 'updatetime' => time() ]); } else { // 失败,增加重试次数 $retry_count = $task['retry_count'] + 1; $status = $retry_count >= 3 ? 3 : 0; // 重试3次后标记为失败 $error_msg = isset($result['return_msg']) ? $result['return_msg'] : '未知错误'; Db::name('icbc_queue')->where('id', $task['id'])->update([ 'status' => $status, 'retry_count' => $retry_count, 'result' => json_encode($result, JSON_UNESCAPED_UNICODE), 'error_msg' => $error_msg, 'updatetime' => time() ]); } } catch (\Exception $e) { // 异常处理 Db::name('icbc_queue')->where('id', $task['id'])->update([ 'status' => 0, 'retry_count' => $task['retry_count'] + 1, 'error_msg' => $e->getMessage(), 'updatetime' => time() ]); \think\Log::record('工行队列处理异常[ID:'.$task['id'].']: ' . $e->getMessage(), 'error'); } } } /** * 写入工行接口日志 * @param bool $is_success 是否成功 * @param array $result 返回结果 * @param array $request_data 请求数据 */ private function writeIcbcLog($is_success, $result, $request_data) { // 确定日志文件路径 $log_dir = RUNTIME_PATH . 'icbc_log/'; if (!is_dir($log_dir)) { mkdir($log_dir, 0755, true); } $filename = $is_success ? 'suc.txt' : 'failicbc.txt'; $filepath = $log_dir . $filename; // 构建日志内容 $log_content = [ '时间' => date('Y-m-d H:i:s'), '请求数据' => $request_data, '返回数据' => $result, '原始响应' => isset($result['raw_response']) ? $result['raw_response'] : '', '分隔线' => str_repeat('-', 80) ]; $log_text = "\n" . json_encode($log_content, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n"; // 追加写入文件 file_put_contents($filepath, $log_text, FILE_APPEND); } /** * 清理响应内容,提取纯JSON * @param string $response 原始响应 * @return string 清理后的JSON字符串 */ private function cleanResponse($response) { // 1. 去除首尾空白 $response = trim($response); // 2. 查找第一个 { 或 [(JSON开始) $json_start = -1; $first_brace = strpos($response, '{'); $first_bracket = strpos($response, '['); if ($first_brace !== false && $first_bracket !== false) { $json_start = min($first_brace, $first_bracket); } elseif ($first_brace !== false) { $json_start = $first_brace; } elseif ($first_bracket !== false) { $json_start = $first_bracket; } // 3. 查找最后一个 } 或 ](JSON结束) $json_end = -1; $last_brace = strrpos($response, '}'); $last_bracket = strrpos($response, ']'); if ($last_brace !== false && $last_bracket !== false) { $json_end = max($last_brace, $last_bracket); } elseif ($last_brace !== false) { $json_end = $last_brace; } elseif ($last_bracket !== false) { $json_end = $last_bracket; } // 4. 提取JSON部分 if ($json_start !== -1 && $json_end !== -1 && $json_end > $json_start) { return substr($response, $json_start, $json_end - $json_start + 1); } // 5. 如果没找到JSON结构,返回原始内容 return $response; } /* * 查看错题 * Robin * @param $ids * */ public function error_ids($ids) { $questions = QuestionModel::whereIn('id', ($ids))->select(); $this->success('', $questions); } }