Golang 实战——微信公众号课程提醒系统

Golang 实战——微信公众号课程提醒系统

起因

最早开始学 Golang 已经是整整一年前了,当时就把基础语法那一块学完了,然后拿 Golang 写了点 leetcode 题。之后由于项目里一直用 Python 和 Java,Golang 这一块就搁置下来没学了。

之前寒假本来是打算学 iOS、Mac 开发这一块的,搞了两个星期,感觉暂时不想学下去了。(我想学 SwiftUI,我觉得这才够酷,但我不想拿赖以生存的老 Macbook 尝试 Catalina,Mojave 写 SwiftUI 没有及时预览,感觉没有灵魂了。)

所以就搬出 Golang 来接着学了。看完了函数、接口、并发这一块,然后就学了一些 Web 开发方面的。(这才实在,不然语言学完就只能刷 leetcode。)

学完了差不多就开学了,刚好我一直憎恶超级课程表广告的烦扰,所以就打算写一个可以自动从教务系统获取课程表、在上课前提醒的课程表项目。这个照理来说是个前端项目,但 iOS 开发这一块还没学完。本来 Android 也行,但我用 iPhone 啊。所以想了个曲线救国的方法——微信公众号开发,纯后端,拿来练习 Golang 再好不过。


由于时间、空间有限很多地方我写的不太清晰。所以在开始阅读本文之前,我建议你打开源码,对照阅读。:https://github.com/cdfmlr/CoursesNotifier


我在这个项目中的很多地方尝试了 Golang 的“面向对象”。Go 不是一个面向对象的语言,这给写惯了 Java、Python 的我们还是带来了一些不适应的。但没关系,正如它的发明者们所说,Go 是用来构建系统的实用语言。面向对象不可否认是构建系统的强有力工具,Golang 当然会有所支持。当然,也只是有所支持,而不是真正的面向对象,我在 coding 的时候,就在一个需要多态的地方碰到了困难,最后不得不更改设计,稍微没那么优雅了。

在这篇文章中,我尝试记录我开发这个系统的整个过程、解释尽可能多的代码设计。但因为毕竟整个项目有接近3000行代码,我不可能逐一解释到位。如果你想看懂所有东西,请去 GitHub 打开这个项目的源码对照来看,我也是个初学者,写出的代码应该还是很简单的。

另外,这篇文章不是 Golang 的入门,在开始阅读前请确保你掌握了(起码是有所了解)以下技能:

目标

我的目的很明确,就是做一个微信公众号系统,在上课前发个通知提醒快上课了。但我不想手动输入课程信息,不然 iDaily Corp 开发的《课程表·ClassTable》就已经很好了。

所以还需要自动从教务系统获取课程表,学校用的新教务系统是:

屏幕快照 2020-03-09 10.35.24

嗯,我研究了一下,他这个web端反爬虫还是做的不错的,可以爬,但不好爬!那我们怎么搞到课表?

还好我发现了这个项目:TLingC/QZAPI。这位大佬爬了强智的 App,抓出了这公司的 API。可以直接调用这个接口获取课表了:

image-20200309170649126

这个 API 文档做的挺好,无可挑剔;但这个 API 着实很恶心,看看他返回的课表:

image-20200309105136147

这就是我们“领先的教学一体化平台”——强(ruo)智教务系统!

我找不到一个合适的、不带个人感情色彩的词语来客观公正地评价这个设计。不管了,也只能将就着用了。


肿的来说,我们的系统有两方面:

  • 一个是输入(I):自动从教务系统获取课表;
  • 还有是输出(O):自动提醒学生上课。

接下来我们就一步一步把这个系统实现:

设计与实现

数据库

首先是数据库设计。

本来写这东西 MongoDB 用挺方便的,但这学期有数据库课嘛,肯定不学这些 NoSQL,所以还是复习一下 SQL,用一下关系型数据库。

其实这个东西挺传统啊,就是数据库书上的例子嘛,主要就三个表:

  • 一个 Student 表,存学号、微信号(公众号里的openid)还有教务密码(这个可以不存的,存了还不安全,我不知道我设计的时候是怎么想的,后悔了,但懒得改😂)
  • 一个 Course 表,存课程名、上课时间、教室地点、授课老师…(就是强智API返回的那些)
  • 还有就是 S-C 选课关系表。
  • 最后,还有一个储存当前是那个学期之类的信息的表。

来看最后设计好的表结构:

屏幕快照 2020-03-04 16.03.36

数据模型

有了数据库,我们还要在程序里使用数据库。也就要把数据库里的记录对应到程序里的结构体(Models)中。

为了方便(懒),我们直接把数据库里的东西对应过来,弄成这几个模型,里面的属性和数据库的属性一一对应(那个current太简单了,就是一个时间嘛, time.Time直接就可以用了,不用再去封装了):·Student

  • Course
  • Relationship

屏幕快照 2020-03-04 16.28.03 2

蓝T是结构体,下面的黄f是属性,红f是函数/方法

(这些图都是从 IDEA 截图出来自己随手拼的,没时间好好调,所以有点丑)

(对,没错,我用 IDEA 写 Golang,MacBook 硬盘小鸭,没办法,咱坚强的 IDEA 带上几个插件就肩负起了我家 Java、Android、Python、Go 的所有“大型“项目开发)

注意这里强智系统API请求来的课程是没有 cid 的,但我们为了唯一识别一个课程,所以在构造函数 NewCourse 里自动通过计算 Name,Teacher,Location,Begin,End,Week,When 的 md5 和得出。

有了模型,我们再实现数据操作(Data) :StudentDatabaseCourseDatabaseStudentCourseRelationshipDatabase

这几个东西实现数据库与模型的转化,提供增删改查操作。

屏幕快照 2020-03-04 16.28.03 3

(这里有很多方法其实都是不必要的,都是一样的操作,我只是一开始为了图方便,复制粘贴出来的)

教务API&Client

有了这些数据模型,我们就可以访问强智教务系统了。

我们先用 Golang 把【强智教务系统API文档】(TLingC/QZAPI)里我们需要用到的接口封装一下。我们需要用到的只是“课程信息”,但使用“课程信息”,又需要我们请求“登录”和“时间信息”。所以我们需要封装这三个请求。

阅读这个强智教务系统API文档,我们会发现所有请求都是类似的 GET:

GET http://jwxt.xxxx.edu.cn/app.do?method=...&...

request.header{token:'运行身份验证authUser时获取到的token,有过期机制'},
request.data{
    'method':'登录/时间/课程信息等的方法名',
    '...':  '一些特定的参数'
    ...
}

所以我们可以把这种 “强智 API GET” 封装起来,做成一个 qzApiGet 函数,简化后面的工作。这个函数通过给定学校域名(就是jwxt.xxxx.edu.cnxxxx,例如华电是ncepu)、token(如果是登录不需要token就传空字符串"")、还有解析请求结果的结构体实例res、以及一个请求参数的map(就是method那些):

func qzApiGet(school string, token string, res interface{
   }, a map[string]string) error {
   
	// Make URL
	rawUrl := fmt.Sprintf("http://jwxt.%s.edu.cn/app.do", school)
	Url, err := url.Parse(rawUrl)
	
    // Add params
	params := url.Values{
   }
	for k, v := range a {
   
		params.Set(k, v)
	}
	Url.RawQuery = params.Encode()
	urlPath := Url.String()

	// Make Request
	client := &http.Client{
   }
	req, err := http.NewRequest("GET", urlPath, nil)
    
    // Add token
	if token != "" {
   
		req.Header.Add("token", token)
	}

	// GET and Parse Response
	resp, err := client.Do(req)
	if err != nil {
   
		log.Println(err)
		return err
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	err = json.Unmarshal(body, res)
    
    // Handle Error and Return
	if err != nil {
   
		log.Println(err)
		return err
	}
	return nil
}

有了这个 qzApiGet,我们就可以方便地封装出我们需要的三个请求了:

屏幕快照 2020-03-04 16.28.03 4

这里面的个函数大同小异,我举其中一个例子就好了:

func GetCurrentTime(school, token, currDate string) (*GetCurrentTimeRespBody, error) {
   
	resp := &GetCurrentTimeRespBody{
   }
	q := map[string]string{
   
		"method":   "getCurrentTime",
		"currDate": currDate,
	}
	err := qzApiGet(school, token, resp, q)
	if err != nil {
   
		log.Println(err)
		return &GetCurrentTimeRespBody{
   }, err
	}
	return resp, nil
}

这些都是一些相当于面向对象里的 public static 的方法啊,调用起来还是不方便。

我们希望有一个 QzClient,这个东西的实例就像我们使用 app 一样的一个客户端,给这个客户端用户名、密码他就可以登录了,然后你要课表就直接取这个实例的 Courses 属性,一切请求都在黑箱里完成。

我们把这个 Client 写出来:

image-20200309173318272

。。。这乍一看,还是很可怕的。没关系,我们一个个慢慢解释。

首先说属性,

  • Student:就是来用这个强智客户端的学生,
  • token:是该学生登录后获取的 token,
  • CurrentXnxqId:表示当前学年学期Id(别问我为什么这么命名,找强智公司去!),
  • CurrentWeek:当前周次
  • Courses:就是当前学期这个同学的所有课程啦,因为需要去重,所以我直接用了一个 map,key 放我们的 Cid(md5和),value 是 Course ,这样就保证了数据不重复。

再来看方法:

  • AuthUser:调用我们 强智 api 里的 AuthUser,登录强智系统,获取操作 token,在该 token 过期之前可以做其他操作;
  • FetchCurrentTime:调用 强智api 获取当前学期、周次,储存在 CurrentXnxqIdCurrentWeek 里;
  • FetchWeekCoursesSlowly:获取某个星期的课表,要反爬虫,所以里面放了一个Sleep,速度很慢,用 chan 去“返回”结果。
  • FetchAllTermCourses,对一个学期的每个周调用 FetchWeekCoursesSlowly,获取真学期的课表,并通过 appendCourse 把这些课程添加到结构体的 Courses 属性中。
  • Save:分别调用 saveStudent, saveCourses, saveSCRelationships 把这个客户端的学生、课程、选课关系写入库!这就是我们唯一写入数据的地方!

呼——终于写完这些了,这里有点枯燥,用强智烂系统的恶心 API 嘛,没多少意思。

小结一下,到现在为止,我们有了数据模型、数据库,可以访问教务系统、从教务系统自动获取给定学生的课表,并把学生、课程、选课关系写入数据库了。

接下来就比较有意思了,我们来看课程时钟的设计。

课程时钟

啥?课程时钟?什么是课程时钟?就是说,咱们要在上课前提醒同学们上课,这就需要服务器知道现在是什么时间、上课在什么时间。也就是一个像“钟”一样的东西,不停地走,在指定的几个时间点“响”(提醒上课),所以我们把这个模块叫做课程时钟——CourseTicker。

这个 CourseTicker 的实现很简单也很直观,就像钟“滴答滴答”地走嘛。CourseTicker 需要定时启动,检测当前是不是快上课了,如果是,就提醒学生,不是就什么都不做。

在 Go 中,要实现这样的定时启动任务很方便,只需要在一个 for 无限循环中睡眠一段时间,然后启动执行任务即可,当然,我们不希望这样永不停止的任务运行在主线程中,所以用一个「匿名函数立即执行」手法把它包装起来:

go func() {
   
    for {
   
        // 计算下一个执行时间
        now := time.Now()
        next := now.Add(t.period)
        // 等待到时间
        timer := time.NewTimer(next.Sub(now))
        <-timer.C
        // 执行任务
        RunTickTask()
    }
}()
抽象周期运行器

上面这个方法虽然做到了不停运行、定时执行,但是我们不方便控制它的开始、结束,而且这段代码也不方便复用。所以我们考虑封装一个可以控制开始、结束,能不停运行、定时执行的 Ticker 结构体(相当于 OOP 的类):

  • 9
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
根据引用,可以通过接入微信公众号后台源码来实现在公众号聊天窗口发消息,并收到AI的回答。具体的实现步骤可以参考相关博客文章,确保修改个人的api-key和公众号key。这样,就可以运行并测试公众号后台的代码,实现查询功能。 此外,根据引用,还可以使用微信公众号文章HTML代码导出工具,实现对微信公众号文章内容区HTML代码的抓取和标签过滤。通过手动替换和删除文章中的图片,可以解决微信后台上传图片防盗链的问题。 需要注意的是,以上都是基于开源工具和源码进行操作,可能需要一些技术和服务器的支持。请确保已准备好相应的环境和资源。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [openAI接入微信公众号后台源码(golang版本)](https://download.csdn.net/download/qq8864/87448657)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [wechat2html:微信公众号文章HTML代码导出工具](https://download.csdn.net/download/weixin_42181319/18415058)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [开源校园综合服务微信小程序源码 包括快递代取、打印服务、校园跑腿、代替服务、上门维修和其他帮助](https://download.csdn.net/download/winkexin/88244323)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值