const { URLSearchParams } = require('url') const { dataEncrypt, dataDecrypt, dataSign } = require('../../plugin/Lepao/Crypto') const { postLepaoSchool } = require('./lepaoSchoolHttp') const BASE_URL = 'https://lepao.ctbu.edu.cn/v3/api.php' const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Linux; Android 16; 2211133C Build/BP2A.250605.031.A3; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.180 Mobile Safari/537.36 XWEB/1380347 MMWEBSDK/20250202 MMWEBID/1020 wxwork/5.0.6.66174 MicroMessenger/8.0.28.48(0x28001c30) MiniProgramEnv/android Luggage/3.0.2.95ef3f83 NetType/WIFI Language/zh_CN ABI/arm64' function lepaoTimestamp() { return Number((Date.now() / 1000).toFixed(3)) } function lepaoNonce() { return String(Math.floor(Math.random() * 900000 + 100000)) } function buildSignedRaw(account, extraFields = {}) { const studentNum = account.student_num || account.student_id const raw = { school_id: account.school_id, term_id: 0, course_id: 0, class_id: 0, student_num: studentNum, card_id: studentNum, uid: account.uid, token: account.token, timestamp: lepaoTimestamp(), version: 1, nonce: lepaoNonce(), ostype: 5, ...extraFields } raw.sign = dataSign(raw) return raw } function unwrapLepaoResponse(result) { if (result?.data && Number(result.is_encrypt) === 1) { const decrypted = dataDecrypt(result.data) if (!decrypted) { const err = new Error('官方乐跑接口解密失败') err.code = 'LEPAO_DECRYPT_FAILED' throw err } result.data = JSON.parse(decrypted) } const failedByStatus = Object.prototype.hasOwnProperty.call(result || {}, 'status') && Number(result.status) !== 1 const failedByCode = Object.prototype.hasOwnProperty.call(result || {}, 'code') && Number(result.code) !== 1 && Number(result.code) !== 200 if (failedByStatus || failedByCode) { const msg = result?.info || result?.msg || '官方乐跑接口请求失败' const err = new Error(msg) err.code = Number(result?.status) === 101 ? 'LEPAO_LOGIN_EXPIRED' : 'LEPAO_OFFICIAL_FAILED' err.result = result throw err } return result } async function postLepaoApi(pathSuffix, account, extraFields = {}, options = {}) { const raw = buildSignedRaw(account, extraFields) const form = new URLSearchParams() form.append('ostype', '5') form.append('data', dataEncrypt(JSON.stringify(raw))) const res = await postLepaoSchool(`${BASE_URL}${pathSuffix}`, form, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': '*/*', 'Accept-Language': 'zh-CN,zh-Hans;q=0.9', 'Accept-Encoding': 'gzip, deflate, br', 'Referer': 'https://servicewechat.com/wxf94c4ddb63d87ede/32/page-frame.html', 'User-Agent': account.userAgent || DEFAULT_USER_AGENT }, timeout: options.timeout || 20000, logger: options.logger }) return unwrapLepaoResponse(res.data) } async function getCurrentTerm(account, options = {}) { const result = await postLepaoApi('/WpRun/getTermList', account, {}, options) const list = Array.isArray(result.data) ? result.data : [] const current = list.find(item => String(item.status) === '1') if (!current?.term_id) { const err = new Error('未找到当前学期') err.code = 'LEPAO_TERM_NOT_FOUND' throw err } return current } async function getTermRunRecord(account, { termId, page = 1 } = {}, options = {}) { const currentPage = Number(page) return postLepaoApi( '/WpRun/getTermRunRecord', account, { term_id: String(termId), page: Number.isFinite(currentPage) && currentPage > 0 ? currentPage : 1 }, options ) } module.exports = { getCurrentTerm, getTermRunRecord }