[{"data":1,"prerenderedAt":420},["ShallowReactive",2],{"/2026/yuketang-deep-dive":3,"surround-/2026/yuketang-deep-dive":413},{"id":4,"title":5,"body":6,"categories":383,"date":385,"description":386,"draft":387,"extension":388,"image":389,"meta":390,"navigation":391,"path":392,"permalink":393,"readingTime":394,"recommend":399,"references":393,"seo":400,"sitemap":401,"stem":402,"tags":403,"type":393,"updated":385,"__hash__":412},"content/posts/2026/yuketang-deep-dive.md","工小湖：给雨课堂与学校教务做了个小程序",{"type":7,"value":8,"toc":372},"minimark",[9,13,16,21,24,29,32,36,39,50,58,65,68,93,96,98,102,105,112,115,123,125,129,133,136,139,151,154,174,178,181,183,187,190,194,197,227,229,233,236,239,253,287,289,293,296,302,322,324,328,331,363,368],[10,11,12],"p",{},"清华开发的雨课堂官方小程序有多卡，上过大学早八的应该都深有体会：开课全班举着手机对大屏幕动态码对焦半天、切后台看个微信回来直接白屏重载、测验倒计时差一秒没点上就锁屏交不了卷、两百多页的课件划两下手机就开始发烫……",[10,14,15],{},"既然如此，不如彻底抓包摸清协议，把核心逻辑抽出来，用微信小程序原生组件手搓一套轻量、秒开的客户端。",[17,18],"pic",{"caption":19,"src":20},"工小湖顶栏：实时感知开课状态","https://img.jiclub.site/file/1792241071949_01_status_motto.png",[22,23],"hr",{},[25,26,28],"h2",{"id":27},"_1-免扫码秒进班","1. 免扫码秒进班",[10,30,31],{},"每节课都要扫大屏幕二维码，其实完全是多此一举。",[17,33],{"caption":34,"src":35},"首页开课识别：检测到开课自动提示，一键直达课堂","https://img.jiclub.site/file/1792241070901_02_home_live_class.png",[10,37,38],{},"抓包看了下投屏端的二维码，本质就是个带短期时间戳签名的临时进班链接：",[40,41,46],"pre",{"className":42,"code":44,"language":45},[43],"language-text","https://changjiang.ykt.io/v2/web/join_lesson?lesson_id=123456&sign=ab8f...&ts=1729158000\n","text",[47,48,44],"code",{"__ignoreMap":49},"",[10,51,52,53,57],{},"这玩意儿主要是给",[54,55,56],"strong",{},"旁听生","扫的。",[10,59,60,61,64],{},"正经在读选了课的学生，学号本来就在班级名单里。老师端一旦点“开始上课”，后台这条课堂记录的状态就变成了进行中（",[47,62,63],{"code":63},"status: 1","）。",[10,66,67],{},"进班逻辑其实就三步：",[69,70,71,79,86],"ol",{},[72,73,74,75,78],"li",{},"小程序冷启动或切回前台时，调 ",[47,76,77],{"code":77},"get_user_course_list"," 查课表；",[72,80,81,82,85],{},"过滤出处于 ",[47,83,84],{"code":84},"status == 1"," 的正在上课的课堂；",[72,87,88,89,92],{},"拿本地的 ",[47,90,91],{"code":91},"session_key"," 直接换取课堂凭据入班。",[10,94,95],{},"上课只要打开小程序，顶栏就会提示已进班，完全不用再抬手举着手机找角度扫大屏。",[22,97],{},[25,99,101],{"id":100},"_2-账号绑定30-秒阻塞长轮询","2. 账号绑定：30 秒阻塞长轮询",[10,103,104],{},"首次把雨课堂绑定到工小湖，需要走一次微信扫码鉴权。",[10,106,107,108,111],{},"如果用前端写 ",[47,109,110],{"code":110},"setInterval"," 每秒去轮询一次接口，不仅发一堆无意义的 HTTP 请求，而且用户在手机点完“确认登录”后，通常要等个 1~2 秒才能跳页面，顿挫感很强。",[10,113,114],{},"这里我把后端中继做成了和官方一致的 30 秒阻塞长轮询：前端发一次请求，后端把连接挂起最多 30 秒。用户手机按下确认的瞬间，服务端立刻返回凭证，页面瞬间完成跳转。",[40,116,121],{"className":117,"code":119,"language":120,"meta":49},[118],"language-python","import httpx\nfrom typing import Optional, Dict, Any\n\nclass RainClassroomAuthService:\n    \"\"\"雨课堂扫码登录长轮询服务\"\"\"\n\n    def __init__(self, base_url: str = \"https://changjiang.ykt.io\") -> None:\n        self.base_url = base_url\n        self.client = httpx.AsyncClient(timeout=35.0)\n\n    async def poll_auth_status(self, ticket: str) -> Optional[Dict[str, Any]]:\n        \"\"\"\n        保持30秒阻塞长连接，监听微信扫码授权状态\n        :param ticket: 二维码生成时获得的票据\n        :return: 授权成功的凭据字典，超时或失败返回 None\n        \"\"\"\n        url = f\"{self.base_url}/custom/auth/v1/auth/status\"\n        params = {\"ticket\": ticket, \"format\": \"json\"}\n\n        try:\n            # NOTE: 保持长连接挂起，扫码确认瞬间立即触发返回，实现0延迟跳转\n            resp = await self.client.get(url, params=params)\n            resp.raise_for_status()\n            res_data = resp.json()\n\n            if res_data.get(\"data\", {}).get(\"status\") == \"success\":\n                return {\n                    \"userId\": res_data[\"data\"][\"user_id\"],\n                    \"sessionKey\": res_data[\"data\"][\"session_key\"],\n                }\n            return None\n        except httpx.RequestError:\n            return None\n","python",[47,122,119],{"__ignoreMap":49},[22,124],{},[25,126,128],{"id":127},"_3-高级设置超时补答与草稿防丢","3. 高级设置：超时补答与草稿防丢",[17,130],{"caption":131,"src":132},"设置面板：支持自动签到与超时补答开关","https://img.jiclub.site/file/1792241069155_04_advanced_sign_settings.png",[10,134,135],{},"官方网页端最搞人心态的一点：倒计时一旦归零，前端 JS 立刻弹个不可关闭的遮罩把输入框死死锁住。很多同学主观题刚在草稿纸算完，输入法还没收起就直接判了白卷。",[10,137,138],{},"但抓包后发现：",[140,141,142,148],"ul",{},[72,143,144,147],{},[54,145,146],{},"服务端并没有在倒计时归零时掐死接口","。考虑到客户端时钟漂移和基站网络延迟，服务端留了较宽松的缓冲期；",[72,149,150],{},"只要老师没有点“结束课堂”或者“生成成绩分析”，提交答案的接口其实一直能用，且重复提交只会幂等覆盖。",[10,152,153],{},"因此在小程序端，我重做了答题交互：",[69,155,156,162,168],{},[72,157,158,161],{},[54,159,160],{},"本地实时存草稿","：每敲一个字自动暂存本地 Storage，哪怕误触退出或者网络断开，重新打开草稿还在；",[72,163,164,167],{},[54,165,166],{},"多图直传","：主观题支持直接拍 9 张草稿图直传；",[72,169,170,173],{},[54,171,172],{},"超时补交","：倒计时结束后，界面虽然标为“可补交”，但提交按钮依然可用，依然能把答案送达后台。",[17,175],{"caption":176,"src":177},"主观题作答：倒计时超时后仍支持补交，支持 9 张解题草稿图","https://img.jiclub.site/file/1792241066942_03_quiz_timeout_makeup.png",[10,179,180],{},"顺便做了一个自动签到：只要检测到开课广播，自动调接口完成打卡，弹出一条轻量提示，避免课前打盹漏签。",[22,182],{},[25,184,186],{"id":185},"_4-课件预览虚拟列表-webp","4. 课件预览：虚拟列表 + WebP",[10,188,189],{},"很多老师动辄放两三百页 PPT，官方网页翻页卡、缩放难，稍微拖两下就闪退。",[17,191],{"caption":192,"src":193},"课件流：支持百页课件拖拽预览与手势无级缩放","https://img.jiclub.site/file/1792241072350_05_slides_bgp_preview.png",[10,195,196],{},"为了让课件看起像本地相册一样丝滑，做了几处处理：",[140,198,199,205,215,221],{},[72,200,201,204],{},[54,202,203],{},"清洗无用字段","：课件接口返回的冗长结构全部砍掉，只留关键的页码与图片链接；",[72,206,207,210,211,214],{},[54,208,209],{},"虚拟列表渲染","：小程序 ",[47,212,213],{"code":213},"scroll-view"," 不做几百张图全量加载，只挂载屏幕可见区前后的几页，内存占用非常平稳；",[72,216,217,220],{},[54,218,219],{},"WebP 压缩转换","：服务端统一做一层 WebP 格式转码，图片流量相比原版降低了 60% 以上；",[72,222,223,226],{},[54,224,225],{},"原生手势","：支持双指无级缩放与拖动，不会卡死网页。",[22,228],{},[25,230,232],{"id":231},"_5-课件与测验分层","5. 课件与测验分层",[10,234,235],{},"官方网页把课件和测验全搅在一页里，弹题时课件直接看不见。",[10,237,238],{},"工小湖把它们拆成了两个独立 Tab：",[140,240,241,247],{},[72,242,243,246],{},[54,244,245],{},"提前预览题目","：抓包发现随堂客观题列表其实是一起推下来的，提前捕获到“习题”栏里，想预习可以提前看；",[72,248,249,252],{},[54,250,251],{},"弹窗即点即交","：老师真正发题时，用轻量的底部弹窗弹出，选项一点直接提交，完全不遮挡正在看的课件页面。",[254,255,257,258],"div",{"style":256},"width: 100%; max-width: 100%; min-width: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; box-sizing: border-box; margin: 1.2rem 0;","\n  ",[254,259,261,262,261,278,257],{"style":260},"display: flex; gap: 14px; width: max-content; padding: 4px 4px 14px;","\n    ",[263,264,266,267,266,273,261],"figure",{"style":265},"flex: 0 0 240px; width: 240px; margin: 0; text-align: center;","\n      ",[268,269],"img",{"src":270,"alt":271,"style":272},"https://img.jiclub.site/file/1792241074493_06_quiz_list_python.png","随堂习题列表","width: 240px; height: auto; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.12);",[274,275,277],"figcaption",{"style":276},"margin-top: 6px; font-size: 0.8em; color: var(--c-text-2, #888);","随堂习题列表：提前看待发题",[263,279,266,280,266,284,261],{"style":265},[268,281],{"src":282,"alt":283,"style":272},"https://img.jiclub.site/file/1792241077211_07_quiz_single_choice.png","单选题弹窗作答",[274,285,286],{"style":276},"原生弹窗作答：点选即交，不遮挡课件",[22,288],{},[25,290,292],{"id":291},"_6-数据安全与隐私","6. 数据安全与隐私",[10,294,295],{},"做校园类工具，账号安全是红线：",[40,297,300],{"className":298,"code":299,"language":45},[43],"[ 客户端微信小程序 (本地加密存储) ]\n                │\n                ▼\n[ 工小湖轻量网关 (纯内存流式中继) ]\n                │\n                ▼\n[ 雨课堂 / 教务云端 ]\n",[47,301,299],{"__ignoreMap":49},[140,303,304,310,316],{},[72,305,306,309],{},[54,307,308],{},"凭证本地加密","：账号密码或 Token 使用 AES 加密存在手机本地 Storage，绝不上传到服务器数据库；",[72,311,312,315],{},[54,313,314],{},"无状态中继","：网关只充当网络请求代理转发，不留存任何学生隐私数据；",[72,317,318,321],{},[54,319,320],{},"界面脱敏","：自习室和课堂名单等涉及姓名的公开信息，统一做掩码打码展示。",[22,323],{},[25,325,327],{"id":326},"_7-体验方式","7. 体验方式",[10,329,330],{},"如果你也受够了官方网页的卡顿和误超时，可以在微信直接体验：",[69,332,333,343,349],{},[72,334,335,338,339,342],{},[54,336,337],{},"微信搜一搜","：搜索 ",[54,340,341],{},"「工小湖」"," 即可找到小程序；",[72,344,345,348],{},[54,346,347],{},"扫码进入","：使用微信扫描下方小程序码直接打开；",[72,350,351,354,355,362],{},[54,352,353],{},"视频演示","：前往哔哩哔哩观看完整实机效果 ",[356,357,361],"a",{"href":358,"rel":359},"https://www.bilibili.com/video/BV11WJe6WEhK/",[360],"nofollow","【工小湖】雨课堂神器，彻底让你的课堂起飞","。",[364,365],"video-embed",{"id":366,"type":367},"BV11WJe6WEhK","bilibili",[17,369],{"caption":370,"src":371},"微信扫一扫或搜一搜「工小湖」","https://img.jiclub.site/file/1792241080029_08_qrcode_search.jpg",{"title":49,"searchDepth":373,"depth":373,"links":374},4,[375,377,378,379,380,381,382],{"id":27,"depth":376,"text":28},2,{"id":100,"depth":376,"text":101},{"id":127,"depth":376,"text":128},{"id":185,"depth":376,"text":186},{"id":231,"depth":376,"text":232},{"id":291,"depth":376,"text":292},{"id":326,"depth":376,"text":327},[384],"经验分享","2026-10-17 18:30:00","梳理雨课堂核心接口，搞定免扫码秒进班、超时补交测验、课件流畅拖拽预览，聊聊在「工小湖」里重构雨课堂体验的技术细节。",false,"md","https://img.jiclub.site/file/1792241072122_cover.jpg",{},true,"/2026/yuketang-deep-dive",null,{"text":395,"minutes":396,"time":397,"words":398},"9 min read",8.04,482400,1608,1,{"title":5,"description":386},{"loc":392},"posts/2026/yuketang-deep-dive",[404,405,406,407,408,409,410,411],"工小湖","雨课堂","逆向工程","架构设计","微信小程序","Python","协议分析","性能优化","FCzQQIDBaPmxkRWBRnNqhjK155rLyGrJV4ID3CcLS54",[414,393],{"title":415,"path":416,"stem":417,"date":418,"type":419,"children":-1},"我把大二的一年，留在了「心元」里","/2026/dev-journey-xodn-flutter-miniprogram","posts/2026/dev-journey-xodn-flutter-miniprogram","2026-09-15 18:30:00","story",1792244456889]