news 2026/10/3 10:54:57

安卓端模拟登录教务系统:OkHttp会话管理与课表解析实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
安卓端模拟登录教务系统:OkHttp会话管理与课表解析实战

简介:一款专为四川大学学生设计的安卓课程表应用源码包,其核心功能是模拟登录学校教务系统,安全获取并清晰展示个人课程表,有效解决课程信息分散、手动查询繁琐的问题。压缩包内共收录四百二十三个文件,整体大小约四点七五兆字节,主要构成为安卓源代码、界面布局描述以及工程构建配置,同时兼顾底层模块、数据配置和说明文档,共同组成一套可直接编译运行的完整项目。目前已有三十一人学习下载,适合具备安卓基础、希望研究教务系统交互流程或需要课程设计参考的开发者。包内附带详细使用说明与疑难解答,有助于快速理解项目结构;项目按照登录验证、数据解析、界面更新等职责划分模块,逻辑清晰。通过阅读源码,可以掌握账户模拟、会话保持、课程数据解析以及列表展示等常见安卓开发技术,对于完成移动应用课程设计或进行个性化二次开发具有很好的参考价值。

1. 教务系统数据搬进安卓:ScuTimetable解决的学生课表痛点

每个学期开学选课完,川大学生都要做同一件事:登教务系统、查课表、对着网页截图,再把图存进手机。调课通知一出来,截图作废,又得重来。ScuTimetable这类应用的出发点就是把整条链路自动化——安卓端模拟登录川大教务系统,安全拿回个人课表页面,在本地解析出课程名、教师、教室、周次和节次,最后用一块按周维度排布的网格界面呈现。适合三类人:被截图课表折腾到崩溃的在校生、想练安卓网络层和HTML解析的开发者、想弄懂模拟登录会话管理的逆向爱好者。

2. 模拟登录川大教务系统:验证码、Cookie与会话保持

2.1 先理清教务系统的认证链路,再写代码

模拟登录不是拿账号密码怼一个接口那么简单。川大教务系统属于典型的校内信息门户,登录过程通常分三段:打开登录页拿到会话Cookie和验证码图片、提交账号密码加验证码换取登录态、跳转到主页面确认登录成功。拿到验证码之前,先要GET一次登录页,因为验证码图片的URL里带了一个会话ID,POST提交时这个会话ID必须原样带回。这就是“先GET拿Cookie,再POST带Cookie”的完整链路。

第二个关键点是登录页的加密逻辑。常见的教务系统(强智、正方这类商用系统)在提交密码时不会明文发送,而是在页面JS里做一次RSA或MD5加盐,再把加密结果塞进隐藏字段POST出去。所以模拟登录前要在浏览器开发者工具里看Network面板,找到提交表单里的密码参数名,以及对应的加密函数。有的系统在登录页HTML里直接写死一个公钥,有的则是单独请求一个密钥接口。这一步不看明白,后面反复报“用户名或密码错误”就是这儿出的问题。

第三个需要注意的,是登录接口可能校验Referer和User-Agent。我习惯把请求头里的User-Agent固定成Chrome的UA,Referer指向登录页本身,避免服务器按客户端特征做拦截。很多人在这一步翻车:明明参数都对,服务端却返回“非法请求”,其实就是少了Referer。

2.2 用OkHttp搭起带Cookie的登录会话

安卓端实现模拟登录,我一般用OkHttp加Jsoup组合,前者管网络连接,后者管解析HTML。先看登录第一步:GET登录页并把Cookie存进CookieJar。OkHttp自带的CookieJar是个接口,需要自己实现,最简单就是用内存保存:

// 一个最简单的内存CookieJar实现 public class SimpleCookieJar implements CookieJar { private final Map<String, List<Cookie>> cookieStore = new HashMap<>(); @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) { // 服务端Set-Cookie时,按域名保存到内存 cookieStore.put(url.host(), cookies); } @Override public List<Cookie> loadForRequest(HttpUrl url) { // 请求发出前,把该域名下的Cookie原样带回 List<Cookie> cookies = cookieStore.get(url.host()); return cookies == null ? Collections.emptyList() : cookies; } }

这段代码的逻辑很直白:OkHttp收到响应时会把Set-Cookie头传给saveFromResponse,发请求时把loadForRequest返回的Cookie拼进请求头。这里有个隐患,就是按host维度存取,如果同一个域名后续发生Cookie覆盖,旧值会丢。教务系统域名比较单一,影响不大,但如果系统里同时存在多个子域(比如cas登录域名和jwc域名不一样),就得改成按主域维度合并保存,否则登录态会莫名其妙失效。

初始化OkHttpClient时把CookieJar挂上,后续请求就自动带Cookie了:

OkHttpClient client = new OkHttpClient.Builder() .cookieJar(new SimpleCookieJar()) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .followRedirects(true) // 登录成功后通常会302跳转 .build();

followRedirects(true)是必选的,教务系统登录成功后往往做302跳转,如果不跟随跳转,拿到的只是跳转声明而非真正的主页面,下一步解析就没东西可用。超时时间建议给长一点,学校教务系统在选课高峰期的响应速度很不稳定,10秒连接超时、15秒读取超时比较稳妥。

接下来是真正登录的请求。验证码的处理方式有两种:一是把验证码图片显示在界面上让用户手动输入,二是用OCR自动识别。学生端应用建议用前者,OCR在验证码有干扰线时的失败率很高,反而拖慢体验。获取验证码图片的请求要和GET登录页用同一个会话,所以代码顺序上必须是“先GET页面→再GET验证码图→用户输入→POST登录”:

public boolean login(String username, String password, String captcha) { // 1. 访问登录页面,初始化会话Cookie Request getPage = new Request.Builder() .url(LOGIN_PAGE_URL) .addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0") .build(); client.newCall(getPage).execute().close(); // 2. 构造登录表单,密码字段按页面JS的加密方式处理 FormBody form = new FormBody.Builder() .add("username", username) .add("password", encryptPassword(password, getPublicKey())) .add("captcha", captcha) .build(); // 3. 提交登录,Referer必须指向登录页 Request loginReq = new Request.Builder() .url(LOGIN_POST_URL) .addHeader("Referer", LOGIN_PAGE_URL) .post(form) .build(); // 4. 判断登录是否成功:页面是否跳转到主页面或包含指定关键词 try (Response resp = client.newCall(loginReq).execute()) { String html = resp.body() != null ? resp.body().string() : ""; return html.contains("学生个人主页") || html.contains("退出登录"); } }

这段代码里最不能照抄的是encryptPassword和getPublicKey,它们完全取决于教务系统页面JS的实现。我用浏览器控制台跑一遍页面里的加密函数,就能看到它调用了什么算法、用了什么参数,然后在安卓端用Java复刻一遍。如果页面是RSA加密,通常会给出一个1024位的公钥Modulus和Exponent,安卓端用Cipher.getInstance("RSA/ECB/PKCS1Padding")就能对齐。

2.3 登录态不丢:Token与Cookie的安卓侧保存策略

模拟登录和“保持登录”是两件事。应用被用户杀掉再打开,内存CookieJar里的会话早没了,必须重新登录。所以登录成功后,把Cookie里的会话ID持久化到本地,通常是SharedPreferences保存一小段字符串,因为会话ID本身就是KV结构。教务系统大部分还是Cookie会话,不需要额外引入Token机制。

我一般这样设计:登录成功后立刻从CookieJar里取出当前域名的Cookie,把JSESSIONID或SESSION这种关键项写入SharedPreferences。下次App启动时,先往CookieJar里塞回这些Cookie,再试着请求一次主页或课表页,如果返回的是登录页就说明会话失效,引导用户重新登录:

// 保存会话 SharedPreferences sp = context.getSharedPreferences("session", Context.MODE_PRIVATE); sp.edit().putString("jsessionid", sessionId).apply(); // 启动时回填 String sessionId = sp.getString("jsessionid", ""); if (!sessionId.isEmpty()) { // 构造Cookie对象并塞进CookieJar }

这里要预警一个会话安全问题:教务系统Cookie一般不代表长期Token,有效期从几十分钟到一天不等。应用层不应该把账号密码存进SharedPreferences,哪怕是加密存储也有被逆向的风险。只存会话Cookie,失效就让用户重新输密码,这是最安全且运维成本最低的方案。

3. 课程表数据解析:从HTML表格到Course对象

3.1 先判断课表返回的是HTML表格还是JSON

登录进去拿到课表页之前,先别急着写解析器。教务系统课表页有两种典型形态:一种直接把课程表渲染成HTML表格,每个td里塞着课程名、教师、周次和教室;另一种是页面加载时异步请求一个JSON接口,返回结构化数据。区分方法很简单,登录后在浏览器开发者工具里打开课表页,看Network面板里的XHR请求。如果有请求返回JSON,而且响应字段里带着“课程名称”“授课教师”“上课周次”这些内容,那就优先走JSON解析,省掉所有HTML脏活。

川大教务系统历史上以HTML表格为主,新版本才逐步有JSON接口的影子。两种都写出来,先看JSON这种理想情况的处理:解析用org.json或Gson都行,关键是把服务端字段名映射到本地Course对象,常见字段有kcmc(课程名称)、jsm(教师名)、skxq(上课星期)、skjc(上课节次)、zcd(周次)这类拼音缩写。服务端给的字段名不一定是标准英文,所以建议一手抓响应、一手建映射表,宁可多写几个optString也别直接强转丢异常。

如果Network里没有XHR请求,课表就是整段HTML直接rendered在页面里,老老实实走Jsoup。

3.2 用Jsoup把HTML课表切成课程实体

HTML课表的典型结构是一个大table,行是节次,列是星期。每个格子td里可能存在多个课程块,每个课程块包含课程名、教师、周次和教室,往往被p标签或br分隔。解析策略是先定位到课表所在的table,注意别选错——一个页面上可能有“教学计划课表”“个人课表”“调课记录”好几个表格,选目标时要看table的id或class属性,或者找包含“星期一”“星期二”表头的那一个。

// 用Jsoup定位到课表table并逐行解析 Document doc = Jsoup.parse(html); Element table = doc.select("table#kbtable, table.klass-table").first(); if (table == null) { // 常见做法:按表头特征兜底查找 for (Element t : doc.select("table")) { if (t.select("th:contains(星期一)").size() > 0 || t.select("td:contains(星期一)").size() > 0) { table = t; break; } } } Elements rows = table.select("tr"); List<Course> courses = new ArrayList<>(); for (int i = 1; i < rows.size(); i++) { // 跳过表头行 Elements cells = rows.get(i).select("td"); // 第一列通常是节次信息,从第2列开始才是周一~周日 for (int j = 1; j < cells.size() - 1; j++) { Element cell = cells.get(j); Elements courseBlocks = cell.select("div.course-item"); if (courseBlocks.isEmpty()) { courseBlocks = cell.select("p"); } for (Element block : courseBlocks) { // 课程名、教师、周次这几个字段从块内提取 String name = block.select("p.course-name, span.course-name").text(); String teacher = block.select("p.teacher").text(); String weeksStr = block.select("p.weeks").text(); String location = block.select("p.location").text(); if (name.isEmpty() && !block.text().isEmpty()) { name = block.text(); } Course c = new Course(); c.setName(name); c.setTeacher(teacher); c.setLocation(location); c.setWeekday(j); // j就是星期几,1表示周一 c.setStartPeriod(i); // 行号对应当前节次 c.setWeekText(weeksStr); // 原始周次字符串,后面单独解析 courses.add(c); } } }

解析这段HTML有两个踩坑点。第一个是选择器不稳定,很多教务系统课表页的div结构在不同年份改版。我处理的方式是写一个“宽松选择器链”:先精确匹配,匹配不到再退化为“取所有td里的p标签”。“宽进严出”的解析策略更抗改版,但代价是可能把页脚信息也收进来,后面过滤时再按课程名长度和是否含时间关键词清洗。

第二个坑是行号和节次的对应关系。有的课表第一行是“第1节”,有的第一行是“第1-2节”,行号并不直接等于节次号。常见做法是读取每个td块内的节次文本做映射,如果拿不到,就按行号乘2修正——两节连上时,课程实际占两行。

3.3 课程时间坐标映射:星期、节次、周次的统一表达

课表展示是一张二维表,但存储应该抽象成“星期几、第几节、起止周”三个维度,这样App才能灵活支持单周、双周、第6周调课这类动态变化。先处理星期:HTML里一个td天然就落在某一列,列号就是星期号。处理节次:行号不一定等于节次,但通常一个行块对应一个节次输入码,教务系统常见的是“第1-2节”“第3-4节”这样的连排方式,解析时要把起始节次取出来,连排节数也要记下来。

// 周次字符串处理:“1-16周(单)” “2-18周(双)” “1-8周,11-16周” public static Set<Integer> parseWeeks(String text) { Set<Integer> weeks = new HashSet<>(); if (text == null || text.isEmpty()) return weeks; String[] parts = text.replace("周", ",").split(",|,"); for (String part : parts) { String p = part.trim(); if (p.isEmpty()) continue; boolean oddOnly = p.contains("单"); boolean evenOnly = p.contains("双"); // 去掉“单/双”后再按区间拆分 String range = p.replace("单", "").replace("双", "") .replace("(", "").replace(")", "") .replace("(", "").replace(")", ""); if (range.contains("-")) { String[] bounds = range.split("-"); int start = Integer.parseInt(bounds[0].trim()); int end = Integer.parseInt(bounds[1].trim()); for (int i = start; i <= end; i++) { if (oddOnly && i % 2 == 0) continue; if (evenOnly && i % 2 != 0) continue; weeks.add(i); } } else if (!range.isEmpty()) { weeks.add(Integer.parseInt(range)); } } return weeks; }

这个parseWeeks方法我建议放在纯Java层类里,不要耦合任何安卓API,方便单元测试。“1-16周(单)”会解析出1、3、5、7、9、11、13、15这八个点,“2-18周(双)”全在偶数周,“1-16周,19周”这类连写也能被逗号分隔逻辑覆盖。边界坑是“1-2周上,3-4周下”这种描述,教务系统很少这么写,但一旦出现,字符串里会混入中文“上”“下”,需要在拆分时先按关键词截断过滤。

到这一步,Course对象就完整了:课程名、教师、教室、星期、起始节次、连续节次数、生效周份列表。后面不管是在手机上画网格课表,还是导成iCal喂给日历,用的都是这套结构。

4. 安卓端展示与刷新:把Course列表画成周课表

4.1 周课表UI选型:GridLayoutManager加自定义Item

把数据变成课表界面,最常见也最可控的方案是RecyclerView套GridLayoutManager。我不用自定义View硬画整张表,原因很简单:RecyclerView自带复用机制,几十门课滚动起来不卡;GridLayoutManager天然支持跨列合并,连排两节可以设spanSizeLookup让卡片纵向跨两行。

布局上定8列:第0列是节次时间轴,第1到第7列对应周一到周日。行数由一天的节次总数决定,川大一般每天12节,取12行。GridLayoutManager的setSpanSizeLookup有一个关键回调——决定每个位置占用多少列,默认是1,但第一行的表头要占满8列:

GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 8); gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { // 第0行是表头,占满8列 if (position < 7) { return 8; } // 普通课程默认占1列 return 1; } }); recyclerView.setLayoutManager(gridLayoutManager);

这里有个细节:表头星期几那一行,我固定当成一个跨8列的大Item,里面自己分7格画出“周一”到“周日”,比把表头做成7个独立列更省心。时间轴列同理,用一个窄Item画“第1节 08:00”这种文本。

课程卡片的位置计算是核心,要让第2节出现在坐标(第2节,周三)上。RecyclerView的position可以用这个公式换算:

// 8列网格中,position由(行,列)决定 // rowIndex从1开始,第0行是表头 // columnIndex从1开始,第0列是时间轴 int position = rowIndex * 8 + columnIndex;

这个计算公式和Excel单元格定位是一个思路。写入Adapter的数据集需要先把Course转换成“带坐标的显示项”,同一个坐标上有多门课(冲突课程)时,再拆成左右两个半宽卡片。这一步建议在上数据源之前就处理完,不要在onBindViewHolder里临时算坐标,否则每次滚动都要重算一遍,比较浪费。

4.2 课程卡片颜色分配与冲突课程处理

一门课一个颜色在视觉上最直观。但如果直接给每门课随机分配,用户会觉得App不稳定。我采用课程名哈希取色板的方式,同一门课固定同一颜色,调课后颜色也不会乱跳。色板选8种浅色系(带透明度),背景色浅、文字色深,避免整块高饱和色糊住界面。

public static int colorForCourseName(String name) { int hash = name.hashCode() & 0x7fffffff; int index = hash % LIGHT_COLOR_PALETTE.length; return LIGHT_COLOR_PALETTE[index]; }

冲突课程是这个方案里最麻烦的边界。教务系统允许同一时间存在两个课程,比如“体育课项目A”和“体育课项目B”同时在周三第5节,被分到不同班级。网格里一个格子只有一个卡片位置,冲突课程只能并排显示。我在转换阶段检测到同一坐标多门课时,生成一个“容器Item”,里面横向排列多个半宽课程卡;如果一门课纵向跨两节,这个容器Item还要跨两行。实现上就是前面提过的spanSizeLookup再叠加一个纵向占位逻辑,RecyclerView的固定高其实做不到灵活的纵向合并,我一般用GridLayoutManager配合自定义Item布局来解决。

4.3 下拉刷新与增量更新:不用每次全量重建

课表数据不需要每次打开都重新拉一遍。教务系统的排课变化频率极低,一周最多改一两次,全部重新解析既慢又消耗流量。我采用“SwipeRefreshLayout做下拉触发 + SQLite存本地 + 变更检测”的三段式:

// 增量更新:判断新旧课程列表是否一致 public boolean isScheduleChanged(List<Course> oldList, List<Course> newList) { if (oldList.size() != newList.size()) return true; StringBuilder oldKey = new StringBuilder(); StringBuilder newKey = new StringBuilder(); for (Course c : oldList) { // 课程名+星期+节次+周次字符串构成唯一指纹 oldKey.append(c.getName()).append('-') .append(c.getWeekday()).append('-') .append(c.getStartPeriod()).append('-') .append(c.getWeekText()).append(';'); } // newList同样拼接 return oldKey.toString().equals(newKey.toString()) == false; }

这段代码的核心是把每条课表记录压成一行字符串当指纹,比对两个列表的指纹合集。如果指纹一致,说明没变化,刷新时只更新时间戳,不触发UI重建;如果不一致,把新的Course列表整个覆盖进SQLite,再通知Adapter刷新。这种做法在UI上的体验是:几乎每次下拉刷新都“秒结束”,但真正变课的时候一定会看到更新。

数据库表结构我通常这样设计:课程表存id, name, teacher, location, weekday, start_period, period_count, week_text,再加一个semester_id字段区分学期。切换学期时直接按semester_id删旧插新,避免上一学期的课混进来。

5. 避坑与常见问题:模拟登录和解析的5个真实翻车现场

5.1 现象:验证码明明输对了,登录却提示“验证码错误”

第一次写模拟登录,我遇到了这个问题:在Postman里同一个验证码能登录成功,到了安卓端就报错。排查后才发现,问题出在请求顺序上——验证码图片是单独发一个GET请求拉下来的,而登录页本身也有一次GET请求,这两次请求之间CookieJar里的会话ID已经更新了,导致验证码图片绑定的会话和登录提交时的会话不是同一个。

解决方法是拉取验证码图片的请求和POST登录的请求之间,绝对不要穿插其他任何请求;更保险的做法是验证码图片请求直接复用登录页GET返回后CookieJar里的状态,拉完图片马上让用户输入,输入期间不要有任何定时刷新任务。另外一个隐蔽原因:有的系统验证码有有效期,用户盯着图看了十几秒再输,已经过期了,这时重新拉一次验证码再继续。

5.2 现象:课程解析出来都是对的,但界面上的时间全错位

有一回解析出的课程名、老师都正常,但到了网格上全部往下偏移一格,第1节课跑到了第2节的位置。检查发现解析代码里用行号直接当节次号,但课表页面第一行实际是“第1-2节”,行号和节次不是一一对应。

解决方法是给每个课程块单独读取“节次”文本,从文本里提取起始节次,不要依赖行号。如果页面连节次文本都没有,就用行号乘以2减去1作为起始节次(因为两节连上)。这个修正逻辑写完后,我给解析器补了一批单元测试,把“第3-4节、行号2”这样的组合全部断言一遍才敢继续。

5.3 现象:网页能正常看课表,App请求却超时或连接失败

学校教工宿舍的Wi-Fi或者某些校园网环境,对TLS握手有中间设备拦截,安卓端默认的TLS配置在握手时加密套件不一致,导致200毫秒内就被重置连接。这个问题的表象是“只有App挂了,浏览器怎么都好”。

解决方法是给OkHttpClient显式指定TLS版本和加密套件列表,把服务端兼容的套件放前面,同时关闭重定向时的TLS降级警告。但我不建议直接关闭证书校验,那会带来中间人攻击风险。正确的做法是先把线上环境的TLS报告抓出来,看服务端实际支持套件,再在客户端做对应配置。

5.4 现象:密码字段提交过去后总是报密码错误,抓包看却是加密串

后来我发现,教务系统登录页的form里,密码框的name是“password”,但真正提交时被页面JS替换成了RSA加密后的值。我最初直接用明文POST,服务端自然不认。这个坑在浏览器里看不到,必须看提交时的实际请求体。

解决办法是用WebView加载登录页,执行页面里的加密JS拿结果,或者用Jsoup配合Android自带JavaScript引擎,把页面里的公钥取出来在Java层做RSA加密。切记不要自创加密算法硬套,服务端解密逻辑是固定的,用什么算法加密完全是它说了算。

5.5 现象:退出登录后课程还在手机上,甚至能看到别人的课表

退出登录只清掉了内存里的会话,SQLite里的课程数据还在。更危险的是,如果把semester_id存成固定值,换用户登录后旧用户的数据仍然会显示。这个问题的本质是本地数据归属没有和账号绑定。

解决方法是每张表都加账号维度字段,退出登录时按账号清理课表数据,同时清掉SharedPreferences里的会话Cookie。如果是多账号切换场景,课程数据最好按账号分表或加account_id索引,别做成全量共享。

6. 一个调试技巧:用Postman回放登录流程,把解析Bug挡在编码前

6.1 把登录和取课表的请求录成Postman集合

安卓端模拟登录最难受的点是:改一次代码要重新编译安装,每次看结果都隔着一条USB线。我后来把整个登录到取课表的过程在Postman里录成集合,直接在电脑上反复调试。做法是登录教务系统页面,打开Postman的Interceptor或直接手工录入请求,把GET登录页、GET验证码、POST登录、GET课表页四个步骤按顺序存成一个Collection。把校验Cookie的请求放在登录请求之后,就能看到会话ID是否被正确保存和携带。

这里有用的是Postman的Collection Runner,它可以按顺序跑完整个登录链路,每一跳的响应时间和状态码都看得清清楚楚。遇到服务端响应结构变化时,先在Postman里确认新结构,再回去改Jsoup选择器。遇到报错时,直接用Postman的console看请求头、Cookies和响应体,排查效率比在Logcat里翻输出高很多。

6.2 用参数化账号和断言做回归验证

Postman集合里可以定义环境变量,把账号、密码、验证码设为变量。验证码是图片没法自动填,但Postman可以把登录请求拆成两步:先拉验证码图片并手动输入,再提交登录。跑回归时用一个固定验证码图片做离线测试,接口通不通一眼就能判断。

// Postman Tests脚本:登录成功后自动断言会话Cookie存在 pm.test("登录返回abnormal.html", function () { // 服务端返回特定错误页时,响应体里会有提示词 pm.expect(pm.response.text()).to.not.include("验证码错误"); }); // 从响应头里提取Set-Cookie,判断会话是否建立 var setCookie = pm.response.headers.get("Set-Cookie"); pm.expect(setCookie).to.not.be.undefined;

这套脚本跑完后,我的一个习惯是:先让Postman把接口全跑通,再写安卓端代码。后端或教务系统改版时,Postman能第一时间暴露变化,定位到是登录链路还是课表解析的问题,省掉大量来回修改、打包、安装的重复劳动。

6.3 教学系统改版后的验证顺序

最后说一个真实的教训。有一年开学,课表解析全军覆没,所有学生都反馈课表空白。我第一反应是服务端改了课表HTML结构,用Postman重新跑一遍,发现登录正常,但取课表接口直接返回登录页——原来是学期切换后会话有效期变短了,登录和取数据间隔超过5分钟就会失效。安卓端一直复用旧会话,自然拿不到数据。

从那以后我就养成一个习惯:所有课表相关的请求都按“登录会话时间和拉取数据时间之差”设置一个阈值,超过阈值强制重登。现在每次写这类抓取解析功能,我都会保留Postman集合当作“接口回归快照”,教务系统一改版,先在Postman里比对响应差异,确认是结构改动还是参数变化,再动代码。这样一个流程能挡住绝大多数改版带来的翻车现场,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 10:54:57

Codex 接入 Jev 模型实战:API Key 配置、TypeSafe 与 Skill 开发指南

1. 从一条报错说起&#xff1a;为什么“Codex Jev”这个组合值得折腾如果你最近在终端里跑 Codex&#xff0c;大概率见过这条让人血压升高的报错&#xff1a;unexpected status 401 unauthorized: incorrect api key provided: sk-svcac****。或者更绕一点的&#xff1a;cc sw…

作者头像 李华
网站建设 2026/10/3 10:53:57

CSDN付费ZIP解压失败?伪加密、EOCD与编码问题排查指南

简介&#xff1a;这是一个面向有桌面支付集成需求的 .NET 开发者与 H5 页面前端开发者的微信/支付宝支付对接资源包。资源覆盖 C# Winform 端接口封装与 H5 端支付页面&#xff0c;可用于商城订单、会员充值、后台收款等需要打通移动端扫码支付的业务场景&#xff0c;适合刚接触…

作者头像 李华
网站建设 2026/10/3 10:53:31

AI应用进入算账阶段:Token成本观测与优化实战

1. 从"模型越多越好"到"每笔调用都要算账"的转折点 过去两年&#xff0c;我参与过好几个企业级 AI 应用的落地项目&#xff0c;从最早的"能跑通就行"&#xff0c;到后来的"多接几个模型试试效果"&#xff0c;再到最近半年频繁被业务方…

作者头像 李华
网站建设 2026/10/3 10:52:43

AI工程从零开始:构建可维护的大模型应用完整链路

你有没有过这样的体验——用ChatGPT写几段代码、让它帮你润色一封邮件&#xff0c;觉得AI也不过如此&#xff1f;但当你接到一个真正的任务&#xff0c;比如“给公司做一个AI客服助手”“把工单系统接入大模型自动分类”&#xff0c;你会发现事情完全不一样了。模型吐出来的内容…

作者头像 李华
网站建设 2026/10/3 10:52:41

从零开始的AI工程:Prompt、Harness与Agent实战拆解

1. 从零开始的AI工程&#xff1a;这个领域到底在解决什么问题1.1 当"会用AI"变成"能交付AI系统"&#xff0c;差距就出来了这两年我一直在做AI相关项目的落地交付&#xff0c;接触过大量团队后感受特别深&#xff1a;真正卡住大家的地方&#xff0c;早就不再…

作者头像 李华
网站建设 2026/10/3 10:52:40

Python机器学习量化投资研究:数据、算法集成与回测全流程解析

简介&#xff1a;这是一套面向量化投资学习者和金融科技从业者的Python机器学习实战资源&#xff0c;以基本面量化投资为核心场景&#xff0c;集成了线性回归、决策树、随机森林、支持向量机、XGBoost、LSTM等多种算法&#xff0c;覆盖数据预处理、特征工程、模型训练、策略回测…

作者头像 李华