├── .github
└── workflows
│ ├── build.yml
│ ├── check.yml
│ └── clean-cache.yml
├── .gitignore
├── .gitmodules
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── balance-updater
├── Cargo.toml
└── src
│ ├── cache.rs
│ ├── lib.rs
│ ├── pull.rs
│ └── vaccum.rs
├── build-push.sh
├── cache-macro
├── Cargo.toml
└── src
│ └── lib.rs
├── cache
├── Cargo.toml
└── src
│ └── lib.rs
├── captcha
├── Cargo.toml
└── src
│ └── lib.rs
├── kite.toml
├── kite
├── Cargo.toml
└── src
│ ├── config.rs
│ ├── db.rs
│ ├── lib.rs
│ ├── model.rs
│ ├── model
│ ├── badge.rs
│ ├── balance.rs
│ ├── board.rs
│ ├── classroom_browser.rs
│ ├── template.rs
│ └── user.rs
│ └── service.rs
├── kite3.service
├── loader
├── Cargo.toml
└── src
│ └── main.rs
├── rustfmt.toml
├── service-v2
├── Cargo.toml
└── src
│ ├── captcha.rs
│ ├── electricity.rs
│ ├── error.rs
│ ├── lib.rs
│ └── response.rs
└── service-v3
├── Cargo.toml
├── build.rs
└── src
├── authserver.rs
├── authserver
├── client.rs
├── portal.rs
└── tls.rs
├── error.rs
├── lib.rs
├── model.rs
├── service.rs
└── service
├── auth.rs
├── badge.rs
├── balance.rs
├── board.rs
├── captcha.rs
├── classroom_browser.rs
├── gen.rs
├── gen
├── badge.rs
├── balance.rs
├── board.rs
├── captcha.rs
├── classroom_browser.rs
├── exception.rs
├── freshman.rs
├── game.rs
├── ping.rs
├── template.rs
├── token.rs
├── typing.rs
├── user.rs
└── yellow_page.rs
├── ping.rs
├── template.rs
└── user.rs
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and deploy
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | env:
7 | CARGO_TERM_COLOR: always
8 |
9 | jobs:
10 |
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v3
17 | - name: Checkout submodules
18 | run: git submodule update --init --force --recursive --remote
19 |
20 | - name: Install latest stable
21 | uses: actions-rs/toolchain@v1
22 | with:
23 | override: true
24 | toolchain: stable
25 | - name: Install Protoc
26 | uses: arduino/setup-protoc@v1
27 |
28 | # Restore cache
29 | - name: Restore cargo packages
30 | uses: actions/cache@v3
31 | env:
32 | cache-name: cache-cargo-registry
33 | with:
34 | path: "~/.cargo/"
35 | key: ${{ runner.os }}-build-${{ env.cache-name }}
36 | restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}
37 | - name: Restore build product
38 | uses: actions/cache@v3
39 | env:
40 | cache-name: cache-build-product
41 | with:
42 | path: "./target/"
43 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Cargo.lock') }}
44 | restore-keys: |
45 | ${{ runner.os }}-build-${{ env.cache-name }}-
46 | ${{ runner.os }}-
47 |
48 | # Build
49 | - name: Build
50 | uses: actions-rs/cargo@v1
51 | with:
52 | command: build
53 | args: --release --all-features
54 |
55 | push:
56 | needs: build
57 | runs-on: ubuntu-latest
58 | if: |
59 | github.ref == 'refs/heads/master' &&
60 | github.repository == 'SIT-kite/kite-server'
61 |
62 | steps:
63 | - name: Restore build product
64 | uses: actions/cache@v3
65 | env:
66 | cache-name: cache-build-product
67 | with:
68 | path: "./target/"
69 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Cargo.lock') }}
70 | restore-keys: |
71 | ${{ runner.os }}-build-${{ env.cache-name }}-
72 | ${{ runner.os }}-
73 | - name: Strip binary file
74 | run: |
75 | strip -s target/release/loader
76 | ls target/release -lh | grep 'loader$' | awk '{print "Binary size is", $5, "after strip" }'
77 | cp target/release/loader kite-server-v3
78 |
79 | - name: Copy file via SSH
80 | uses: appleboy/scp-action@master
81 | with:
82 | host: ${{ secrets.SERVER_HOST }}
83 | port: ${{ secrets.SERVER_PORT }}
84 | username: ${{ secrets.SERVER_USER }}
85 | password: ${{ secrets.SERVER_SECRET }}
86 | source: kite-server-v3
87 | target: ${{ secrets.DEPLOY_PATH }}/kite-server-v3.tmp
88 | - name: Deploy & Restart via SSH
89 | uses: appleboy/ssh-action@master
90 | with:
91 | host: ${{ secrets.SERVER_HOST }}
92 | port: ${{ secrets.SERVER_PORT }}
93 | username: ${{ secrets.SERVER_USER }}
94 | password: ${{ secrets.SERVER_SECRET }}
95 | # User home is the deployment path.
96 | script: |
97 | cp ./kite-server-v3 ./kite-server-v3.bak
98 | systemctl --user stop kite3.service
99 | mv ./kite-server-v3.tmp/kite-server-v3 kite-server-v3
100 | systemctl --user start kite3.service
101 | rm -rf ./kite-server-v3.tmp
102 |
103 | notification:
104 | runs-on: ubuntu-latest
105 | needs: [ push ]
106 | if: |
107 | github.event_name != 'pull_request' &&
108 | github.repository == 'SIT-kite/kite-server' &&
109 | always()
110 | steps:
111 | - uses: technote-space/workflow-conclusion-action@v2
112 | - uses: nelonoel/branch-name@v1.0.1
113 | - uses: benjlevesque/short-sha@v2.1
114 | id: short-sha
115 | with: { length: 7 }
116 |
117 | - name: 🤖 Build Result Notification
118 | uses: appleboy/telegram-action@v0.1.1
119 | with:
120 | to: ${{ secrets.CHANNEL_CHAT_ID }}
121 | token: ${{ secrets.BOT_TOKEN }}
122 | format: markdown
123 | message: |
124 | The result to 🎉the deployment of kite-server (*${{ env.BRANCH_NAME }}*): *${{ env.WORKFLOW_CONCLUSION }}*
125 | By *${{ github.actor }}*. Commit message:
126 | *${{ github.event.head_commit.message }}*
127 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Rust check
2 |
3 | on:
4 | push: { branches: [ master ] }
5 | pull_request: { branches: [ master ] }
6 |
7 | env:
8 | CARGO_TERM_COLOR: always
9 |
10 | jobs:
11 | check:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v3
17 | - name: Checkout submodules
18 | run: git submodule update --init --force --recursive --remote
19 |
20 | - name: Install latest nightly
21 | uses: actions-rs/toolchain@v1
22 | with:
23 | override: true
24 | toolchain: nightly
25 | - name: Install Protoc
26 | uses: arduino/setup-protoc@v1
27 |
28 | - name: Restore cargo packages
29 | uses: actions/cache@v3
30 | env:
31 | cache-name: cache-cargo-registry
32 | with:
33 | path: "~/.cargo/"
34 | key: ${{ runner.os }}-build-${{ env.cache-name }}
35 | restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}
36 |
37 | - name: Run cargo check
38 | uses: actions-rs/cargo@v1
39 | with:
40 | command: check
41 |
42 | notification:
43 | runs-on: ubuntu-latest
44 | needs: [ check ]
45 | if: |
46 | github.repository == 'SIT-kite/kite-server' &&
47 | always()
48 | steps:
49 | - uses: technote-space/workflow-conclusion-action@v2
50 | - uses: nelonoel/branch-name@v1.0.1
51 | - uses: benjlevesque/short-sha@v2.1
52 | id: short-sha
53 | with: { length: 7 }
54 |
55 | - name: 🤖 Build Success Notification
56 | uses: appleboy/telegram-action@v0.1.1
57 | with:
58 | to: ${{ secrets.CHANNEL_CHAT_ID }}
59 | token: ${{ secrets.BOT_TOKEN }}
60 | format: markdown
61 | message: |
62 | kite-server (*${{ env.BRANCH_NAME }}*) has just checked, *${{ env.WORKFLOW_CONCLUSION }}*.
63 | By *${{ github.actor }}*. Commit message:
64 | *${{ github.event.head_commit.message }}*
65 |
--------------------------------------------------------------------------------
/.github/workflows/clean-cache.yml:
--------------------------------------------------------------------------------
1 | name: Clear cache
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | permissions:
7 | actions: write
8 |
9 | jobs:
10 | clear-cache:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Clear cache
14 | uses: actions/github-script@v6
15 | with:
16 | script: |
17 | console.log("About to clear")
18 | const caches = await github.rest.actions.getActionsCacheList({
19 | owner: context.repo.owner,
20 | repo: context.repo.repo,
21 | })
22 | for (const cache of caches.data.actions_caches) {
23 | console.log(cache)
24 | github.rest.actions.deleteActionsCacheById({
25 | owner: context.repo.owner,
26 | repo: context.repo.repo,
27 | cache_id: cache.id,
28 | })
29 | }
30 | console.log("Clear completed")
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | target/
4 | .runtime-cache/
5 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "proto"]
2 | path = proto
3 | url = https://github.com/SIT-kite/kite-proto.git
4 | [submodule "d4ocr-rust"]
5 | path = d4ocr-rust
6 | url = https://github.com/sunnysab/d4ocr-rust
7 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
2 |
3 | [workspace]
4 | members = [
5 | "cache",
6 | "cache-macro",
7 | "captcha",
8 | "kite",
9 | "loader",
10 | "service-v3",
11 | "service-v2",
12 | "balance-updater",
13 | ]
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 上应小风筝(服务端)
2 |
3 | ## 概要
4 |
5 | 上应小风筝,旨在为 [上海应用技术大学](www.sit.edu.cn) 的学生提供校园信息整合与管理服务,本仓库管理并维护服务端代码。
6 |
7 | > 对于一位大学(本科)生而言,在校时间不过四年左右。而 “小风筝” 项目从 2020 年初疫情爆发时开始编写,到如今的 2023 已历经了
8 | > 3个年头了。项目
9 | > 团队的好友们所经历的 “从 ‘不太会使用 git 和 Github’、‘仅仅听说过 Rust 语言’,到如今虽然代码仍然写得不怎么样,但写出来的东西勉强能看、能用”
10 | > 的时光,令人倍感珍惜与怀念。
11 |
12 | ## 功能
13 |
14 | - [x] 电费查询
15 | - [x] 空教室查询
16 | - [ ] 二手闲置交易
17 | - [x] 入学信息查询
18 | - [ ] 失物招领
19 |
20 | 本版本废弃了一部分更新频率较少的接口,这部分数据将直接以静态方式([仓库](https://github.com/SIT-kite/kite-static))存储,以便于
21 | CDN 分发,同时节约成本、提高访问速度。
22 |
23 | ## 项目结构
24 |
25 | 目录 `proto` 包含了小风筝 gRPC 服务规范的若干通信消息与服务格式。其他项目所使用的 Rust 包如下表:
26 |
27 | | 包 | 说明 |
28 | |-------------|-----------------------------------|
29 | | cache | 读写用户查询时生成的缓存 |
30 | | cache-macro | 简化缓存使用的过程宏 |
31 | | kite | 基础库。提供了服务配置初始化,并提供了 Service trait |
32 | | loader | 程序入口 |
33 | | service-v3 | 小风筝 gRPC 服务 |
34 |
35 | ## 发展
36 |
37 | 让我来回顾一下项目发展的历史吧。
38 |
39 | 由于云计算厂商的服务器一般较为昂贵(他们一般希望 “帮助” 企业节约人工成本,因而设备租用价格并不低),加之经费报销的限制、个人习惯等因素,我们在
40 | 项目开工之初便决定尽可能地节约资源占用。开发之初缺乏有关开发和运营的经验,对 “并发数”、“用户数” 等概念和它背后的含义没有直观了解,我偏执地追
41 | 求着性能优化。我们希望它能在单核 1G 内存的机器上与数据库 PostgreSQL 流畅运行,并承载和选课阶段差不多的访问量。受
42 | [TechEmpower Framework Benchmarks](https://www.techempower.com/benchmarks/) 的影响,我们在最初的版本中开发中选择了 Rust
43 | 语言
44 | 及 [actix-web](https://github.com/actix/actix-web) 框架,彼时它正霸占排行榜的第一位,而且后几位全是它的小弟~
45 |
46 | 2020 年中旬,我们开发出了 kite-server 第一版。9 月,提供 “新生入学查询”(即 “迎新”)服务,前端为微信小程序 “
47 | [上应小风筝](https://github.com/SIT-kite/kite-microapp)”。当时的版本基于 actix-web 1 或 2,上线初发现:
48 |
49 | - reqwest 库对微信 API 服务器的连接无法正常释放,`nestat` 报告了大量处于 `CLOSE_WAIT` 状态的连接
50 | - 程序内存占用过大,猜测存在内存泄漏问题。内存占用 300 - 500M 左右时崩溃
51 |
52 | 在 Rust 中使用数据库和 SQL 这件事不好说难或简单。现成的库不少,社区尤为偏爱 PostgreSQL (现在我也是它的粉丝),但当时主流的
53 | ORM 只有 diesel
54 | (现在还有 SeaORM 等)。我曾为了 diesel 的一个泛型错误找了一天资料,也曾失手将周末写的代码删除。后来因为其对异步的支持不好,以及复杂查询导致
55 | “嵌套地狱” 的关系,项目便改用 sqlx 库了——裸写 SQL 语句也不错,至少有它帮忙把数据库返回的记录行转换成结构体。在 Web
56 | 后端开发过程中,文章匮乏
57 | 使人迷茫,连现成的项目也少得可怜:一般的例子都会带你写一组 TODO 接口,增删改查之后便草草结束——没有大一点的后端项目供我照葫芦画瓢,只能根据经验
58 | 不断地 “重构”,好让代码看起来更舒服一些。有一个 rust-admin 项目,是国人开发的,kite-server 照着它划了几瓢,可惜由于作者身体原因这个项目没
59 | 有进一步完善。
60 |
61 | 为了解决微信小程序访问校园网的问题,我们开发了 [kite-agent](https://github.com/SIT-kite/kite-agent) 程序,以期望它在校内提供中转服务。
62 | 具体是这样的:校园网内的机器(agent)先连接到 server, 后续由 server 将用户请求转发到 agent。绝大多数的 RPC 库都是 client 向
63 | server 建立连
64 | 接并发送请求,而这个(反向的)需求很冷门,一时竟无法解决,于是我基于 bincode (序列化和反序列化库)和 socket 完成了一个简易的
65 | RPC 工具。2021
66 | 年,在一番探索中,我兴奋地发现了 [tower](https://github.com/tower-rs/tower) 框架——它仿佛是为了我们这个需求而生的。暑假时我们就有了基于
67 | tower 的 kite-agent。tower 把服务与底层的连接进行了分离,支持各种中间件,让后端项目的结构上了个档次。从这一点,可以看出 Rust
68 | 的优点:底层。
69 | 据我所知,一些其他语言的框架不太能完成这样的操作,他们默认了 server 就是打开 socket 去监听的那一方。一定程度上来讲,这个需求也可以用
70 | frp、ssh
71 | 转发等方法实现,但当校园网内有多个 agent 实例时,这样的方式不太好做负载均衡。在后期我在 server 中支持了负载均衡的操作,
72 | server 可以将用户请求
73 | 随机地发给一个 agent,但也遇到了另一个问题:我需要一个结构去管理和 agent 的连接,当 agent 断开时,我希望自动从连接池中删除它。因为缺乏开发经验,
74 | 在所有权上和编译器斗智斗勇,这个功能始终没能实现。迫不得已,改成:发现连接中断(此时 send 会报错 broken pipe)时删除该连接。这便是
75 | Rust 基础不
76 | 牢带来的影响。
77 |
78 | 在 Rust 中做爬虫和网页解析更是麻烦。虽然有 Mozilla 开源的神器 html5ever,可 python 的 BeautifulSoup 谁也不想总是做那么底层的操作。直到
79 | 遇到了 scraper,这个库一定程度上简化了对 DOM 的操作,开发体验马马虎虎。
80 |
81 | 在 kite-server 1 和 kite-agent 开发期间,@zhangzqs 和 @B635 加入了开发,主要集中于 kite-agent 的编写。2021 年下半年的时候,我们的小
82 | 程序被校方要求下架,便改行做 App 开发,后端也要随之变动。后端趁机转向 v2 版本 ——基于油条哥的 poem-web 开发。总的来说,开发体验比现有的各个
83 | Rust Web 框架好一点。这期间,因为计划在 App 上直接连接校园网,后端便省去了与 agent 通信的环节,逐渐成为纯粹的 CRUD 机器。
84 |
85 | 在 Web 开发的过程中,慢慢体会到,接口是前后端交流的根据。于是,本分支下的 kite-server 3, 开始采用 tonic 框架,使用 gRPC
86 | 通信。在选型过程中,字节跳动团队开发的 volo 框架不断出现在视野中,但最终没有选择它原因是:
87 |
88 | 1. 生态较弱,最近没太多时间踩坑
89 | 2. 我更倾向于将 protobuf 生成的代码作为项目文件使用,而不是在 service 层使用类似 `incldue_proto!("xxx.proto")` 的方式编译
90 | proto 文件
91 | ——至少在 Clion 上,它将使得 IDE 提示完全失效,开发体验较差。而 tonic-build 给出了指定输出文件的方法。
92 |
93 | 本质上,本分支所管理的 v3 版本也是单纯的 CRUD 版本。可看的是,在这一版本中,将引入一个利用用户端网络和 gRPC 双向流去访问校园网的方法。同时,
94 | 该版本的项目组织与代码结构也更正式,可供编写类似项目的同仁参考。
95 |
96 | ## 环境配置
97 |
98 | 待完善
99 |
100 | ## 有关项目
101 |
102 | | 项目 | 说明 |
103 | |------------------------------------------------------------|---------------|
104 | | [kite-app](https://github.com/SIT-kite/kite-app) | App 前端 |
105 | | [zf-tools](https://github.com/sunnysab/zf-tools) | 正方教务系统爬虫 |
106 | | [kite-string](https://github.com/SIT-Kite/kite-string) | 校园网爬虫工具 |
107 | | [kite-agent](https://github.com/sunnysab/kite-agent) | 后端数据抓取工具(已废弃) |
108 | | [kite-protocol](https://github.com/SIT-Kite/kite-protocol) | 通信协议库(已废弃) |
109 |
110 | ## 如何贡献
111 |
112 | 算了,我都写麻了
113 |
114 | ## 开源协议
115 |
116 | [GPLv3](https://github.com/SIT-Kite/kite-server/blob/master/LICENSE) © 上海应用技术大学易班 小风筝团队
117 |
118 | 除此之外,您不能将本程序用于各类竞赛、毕业设计、论文等。
119 |
120 |
--------------------------------------------------------------------------------
/balance-updater/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "balance-updater"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | kite = { path = "../kite" }
10 |
11 | serde = { version = "1.0", features = ["derive"] }
12 | serde_json = "1.0"
13 | anyhow = "1.0"
14 | chrono = { version = "0.4.23", features = ["serde"] }
15 |
16 | async-trait = "0.1.61"
17 | tokio = { version = "*", features = ["time"] }
18 | sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio-rustls", "chrono", "postgres", "macros"] }
19 | reqwest = { version = "*", features = ["json"] }
20 |
21 | tracing = "0.1"
22 | once_cell = "1.17"
--------------------------------------------------------------------------------
/balance-updater/src/cache.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * 上应小风筝 便利校园,一步到位
3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use kite::cache;
20 | use kite::cache::SCOPE_BALANCE;
21 |
22 | pub fn clear_cache() {
23 | cache::get().erase_keys(SCOPE_BALANCE);
24 | }
25 |
--------------------------------------------------------------------------------
/balance-updater/src/lib.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * 上应小风筝 便利校园,一步到位
3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use std::time::Duration;
20 |
21 | use anyhow::Result;
22 | use tokio::time;
23 |
24 | use kite::service::KiteModule;
25 |
26 | mod cache;
27 | mod pull;
28 | mod vaccum;
29 |
30 | pub struct BalanceUpdater {}
31 |
32 | #[async_trait::async_trait]
33 | impl KiteModule for BalanceUpdater {
34 | async fn run() {
35 | daemon().await.expect("BalanceUpdater exited.");
36 | }
37 | }
38 |
39 | async fn daemon() -> Result<()> {
40 | let db = kite::get_db();
41 |
42 | let duration = Duration::from_secs(60 * 20);
43 | let mut interval = time::interval(duration);
44 |
45 | // Because it's a little hard to write async-callback in rust, I use one loop to handle these two
46 | // events. Maybe it can be rewrite in future.
47 | let mut i = 0;
48 | loop {
49 | interval.tick().await;
50 | if i % 72 == 0 {
51 | // 72 * 20min = one day
52 | if let Err(e) = vaccum::remove_outdated(&db).await {
53 | tracing::error!("Failed to remove outdated consumption record: {e}");
54 | }
55 | i = 0;
56 | }
57 | // pull each 20min
58 | if let Err(e) = pull::pull_balance_list(&db).await {
59 | tracing::error!("Failed to pull balance list: {e}");
60 | }
61 | i += 1;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/balance-updater/src/pull.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * 上应小风筝 便利校园,一步到位
3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use anyhow::Result;
20 | use chrono::Local;
21 | use once_cell::sync::OnceCell;
22 | use serde::de::Error;
23 | use serde::Deserialize;
24 | use sqlx::{Acquire, Connection, PgPool};
25 | use std::collections::{HashMap, HashSet};
26 | use tokio::time::Instant;
27 |
28 | use super::cache::clear_cache;
29 |
30 | #[derive(Deserialize)]
31 | struct RawBalance {
32 | #[serde(rename(deserialize = "RoomName"), deserialize_with = "str2i32")]
33 | room: i32,
34 | #[serde(rename(deserialize = "Balance"), deserialize_with = "str2float")]
35 | total: f32,
36 | }
37 |
38 | type RoomNumber = i32;
39 |
40 | const INVALID_ROOM_NUMBER: RoomNumber = 0;
41 |
42 | fn str2i32<'de, D>(deserializer: D) -> Result
43 | where
44 | D: serde::Deserializer<'de>,
45 | {
46 | let s = String::deserialize(deserializer)?;
47 | // Some room numbers are start with "-" and used for testing, we will ignore them.
48 | if let Ok(n) = s.parse::() {
49 | if get_valid_room_set().contains(&n) {
50 | return Ok(n);
51 | }
52 | }
53 | Ok(INVALID_ROOM_NUMBER)
54 | }
55 |
56 | fn str2float<'de, D>(deserializer: D) -> Result
57 | where
58 | D: serde::Deserializer<'de>,
59 | {
60 | let s = String::deserialize(deserializer)?;
61 | s.parse::().map_err(Error::custom)
62 | }
63 |
64 | static VALID_ROOM_SET: OnceCell> = OnceCell::new();
65 |
66 | async fn init_valid_room_set(db: &PgPool) -> Result<()> {
67 | #[derive(Eq, Hash, PartialEq, sqlx::FromRow)]
68 | struct ValidRoom {
69 | room: RoomNumber,
70 | }
71 |
72 | // Ignore pulling if VALID_ROOM_SET is set.
73 | if VALID_ROOM_SET.get().is_some() {
74 | return Ok(());
75 | }
76 | let room_list: Vec = sqlx::query_as("SELECT id AS room FROM dormitory_room;")
77 | .fetch_all(db)
78 | .await?;
79 |
80 | let set = HashSet::from_iter(room_list.into_iter().map(|s| s.room));
81 | VALID_ROOM_SET.set(set).expect("Failed to set VALID_ROOM_SET");
82 | Ok(())
83 | }
84 |
85 | fn get_valid_room_set() -> &'static HashSet {
86 | VALID_ROOM_SET
87 | .get()
88 | .expect("init_valid_room_set() should be called first.")
89 | }
90 |
91 | async fn request_room_balance() -> Result> {
92 | let client = reqwest::Client::new();
93 | let mut params = HashMap::new();
94 |
95 | static DATA_SOURCE_URL: &str =
96 | "https://xgfy.sit.edu.cn/unifri-flow/WF/Comm/ProcessRequest.do?DoType=DBAccess_RunSQLReturnTable";
97 |
98 | params.insert("SQL", "select * from sys_room_balance;");
99 | let response = client
100 | .post(DATA_SOURCE_URL)
101 | .header("Cookie", "FK_Dept=B1101")
102 | .form(¶ms)
103 | .send()
104 | .await?;
105 |
106 | response.json::>().await.map_err(Into::into)
107 | }
108 |
109 | async fn get_balance_list() -> Result> {
110 | let raw_response = request_room_balance().await?;
111 | let filter = |r: &RawBalance| r.room != INVALID_ROOM_NUMBER;
112 | let result = raw_response.into_iter().filter(filter).collect();
113 |
114 | Ok(result)
115 | }
116 |
117 | async fn update_db(db: &PgPool, records: Vec) -> Result<()> {
118 | let current = Local::now();
119 | let rooms: Vec = records.iter().map(|x| x.room).collect();
120 | let balance: Vec = records.iter().map(|x| x.total).collect();
121 |
122 | // Consumption is calculated by dormitory_balance_trigger on PostgreSQL.
123 | // Do not delete all data before any INSERT statement.
124 | // Here, we use a single SQL statement instead of a for loop to speed up updating process.
125 | sqlx::query(
126 | "INSERT INTO dormitory_balance
127 | (room, total_balance, ts)
128 | SELECT *, $3::timestamptz AS ts FROM UNNEST($1::int[], $2::float[])
129 | ON CONFLICT (room) DO UPDATE SET total_balance = excluded.total_balance, ts = excluded.ts;",
130 | )
131 | .bind(rooms)
132 | .bind(balance)
133 | .bind(current)
134 | .execute(db)
135 | .await
136 | .map(|_| ())
137 | .map_err(Into::into)
138 | }
139 |
140 | async fn update_ranking(db: &PgPool) -> Result<()> {
141 | let mut transaction = db.begin().await?;
142 |
143 | sqlx::query("DELETE FROM dormitory_consumption_ranking;")
144 | .execute(&mut *transaction)
145 | .await?;
146 | sqlx::query(
147 | "INSERT INTO dormitory_consumption_ranking
148 | SELECT * FROM dormitory_do_rank();")
149 | .execute(&mut *transaction)
150 | .await?;
151 |
152 | transaction.commit().await.map_err(Into::into)
153 | }
154 |
155 | pub async fn pull_balance_list(db: &PgPool) -> Result<()> {
156 | init_valid_room_set(db).await?;
157 |
158 | let start = Instant::now();
159 | let result = get_balance_list().await?;
160 | tracing::info!("get {} records, cost {}s", result.len(), start.elapsed().as_secs_f32());
161 |
162 | let start = Instant::now();
163 | let count = result.len();
164 | update_db(db, result).await?;
165 | tracing::info!("save {} records, cost {}s", count, start.elapsed().as_secs_f32());
166 |
167 | update_ranking(db).await?;
168 | clear_cache();
169 | Ok(())
170 | }
171 |
--------------------------------------------------------------------------------
/balance-updater/src/vaccum.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * 上应小风筝 便利校园,一步到位
3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use anyhow::Result;
20 | use sqlx::PgPool;
21 |
22 | pub async fn remove_outdated(db: &PgPool) -> Result<()> {
23 | sqlx::query("DELETE FROM dormitory_balance WHERE ts < current_timestamp - '8days'::interval;")
24 | .execute(db)
25 | .await?;
26 | Ok(())
27 | }
28 |
--------------------------------------------------------------------------------
/build-push.sh:
--------------------------------------------------------------------------------
1 | #
2 | # 上应小风筝 便利校园,一步到位
3 | # Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU General Public License as published by
7 | # the Free Software Foundation, either version 3 of the License, or
8 | # (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU General Public License
16 | # along with this program. If not, see .
17 | #
18 |
19 | cargo build --release
20 | cp target/release/loader kite-server-v3
21 | strip -s kite-server-v3
22 |
23 | scp kite-server-v3 kite@origin.kite.sunnysab.cn:~/kite-server-v3.new
24 | ssh kite@origin.kite.sunnysab.cn \
25 | "systemctl --user stop kite3.service; \
26 | mv kite-server-v3 kite-server-v3.old; \
27 | mv kite-server-v3.new kite-server-v3; \
28 | systemctl --user start kite3.service;"
--------------------------------------------------------------------------------
/cache-macro/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "cache-macro"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | darling = "0.14"
10 | syn = { version = "1.0", features = ["full"] }
11 | quote = "1.0"
12 | proc-macro2 = "1.0"
13 |
14 | [lib]
15 | proc-macro = true
--------------------------------------------------------------------------------
/cache-macro/src/lib.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * 上应小风筝 便利校园,一步到位
3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use proc_macro::TokenStream;
20 |
21 | use darling::FromMeta;
22 | use proc_macro2::TokenStream as TokenStream2;
23 | use quote::quote;
24 | use syn::parse_macro_input;
25 | use syn::punctuated::Punctuated;
26 | use syn::token::Comma;
27 |
28 | const DEFAULT_CACHE_TIMEOUT: i64 = 3600;
29 |
30 | #[derive(Debug, darling::FromMeta)]
31 | struct CacheParameter {
32 | #[darling(default)]
33 | /// Cache timeout option (in second)
34 | timeout: Option,
35 | }
36 |
37 | fn parse_attribute(args: syn::AttributeArgs) -> CacheParameter {
38 | match CacheParameter::from_list(&args) {
39 | Ok(param) => param,
40 | Err(e) => {
41 | panic!("{}", e.to_string());
42 | }
43 | }
44 | }
45 |
46 | fn parse_fn(item: syn::Item) -> syn::ItemFn {
47 | if let syn::Item::Fn(func) = item {
48 | func
49 | } else {
50 | panic!("You should only attach cache attribute to a function.");
51 | }
52 | }
53 |
54 | /// Adapted from:
55 | /// https://stackoverflow.com/questions/71480280/how-do-i-pass-arguments-from-a-generated-function-to-another-function-in-a-proce
56 | fn transform_args(args: &Punctuated) -> TokenStream2 {
57 | let idents_iter = args
58 | .iter()
59 | .filter_map(|arg| {
60 | if let syn::FnArg::Typed(pat_type) = arg {
61 | if let syn::Pat::Ident(pat_ident) = *pat_type.pat.clone() {
62 | return Some(pat_ident.ident);
63 | }
64 | }
65 | None
66 | })
67 | .filter(|ident| {
68 | // Ignore db parameter
69 | let ident = ident.to_string();
70 | ident != "db" && ident != "pool"
71 | });
72 |
73 | let mut punctuated: Punctuated = Punctuated::new();
74 | idents_iter.for_each(|ident| punctuated.push(ident));
75 |
76 | TokenStream2::from(quote!(#punctuated))
77 | }
78 |
79 | #[proc_macro_attribute]
80 | pub fn cache(args: TokenStream, item: TokenStream) -> TokenStream {
81 | let args = parse_macro_input!(args as syn::AttributeArgs);
82 | let item = parse_macro_input!(item as syn::Item);
83 |
84 | // Parse cache parameter
85 | let param = parse_attribute(args);
86 | let timeout = param.timeout.unwrap_or(DEFAULT_CACHE_TIMEOUT);
87 |
88 | // Parse function signature
89 | let syn::ItemFn { attrs, vis, sig, block } = parse_fn(item);
90 | // Parse function parameter
91 | let punctuated_args = transform_args(&sig.inputs);
92 | // Function return type
93 | let ret_type = if let syn::ReturnType::Type(_arrow, ty) = sig.output.clone() {
94 | *ty
95 | } else {
96 | panic!("Unexpected return type");
97 | };
98 |
99 | let result = TokenStream2::from(quote! {
100 | #(#attrs)* #vis #sig {
101 | use chrono::Duration;
102 |
103 | // Query cache
104 | if let Ok(Some(cache)) = kite::cache::cache_query!(key = #punctuated_args; timeout = Duration::seconds(#timeout)) {
105 | return Ok(cache);
106 | };
107 |
108 | // If cache miss, do query operation
109 | let db_result: #ret_type = {#block};
110 | let data = db_result?;
111 |
112 | // Save result to cache
113 | kite::cache::cache_save!(key = #punctuated_args; value = data.clone());
114 | Ok(data)
115 | }
116 | });
117 | result.into()
118 | }
119 |
--------------------------------------------------------------------------------
/cache/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "cache"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | anyhow = "1.0.68"
10 | once_cell = "1.17"
11 | serde = { version = "1", features = ["derive"] }
12 | chrono = { version = "0.4.23", features = ["serde"] }
13 | sled = "0.34"
14 | bincode = "2.0.0-rc.2"
15 | tracing = "0.1"
--------------------------------------------------------------------------------
/cache/src/lib.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * 上应小风筝 便利校园,一步到位
3 | * Copyright (C) 2020-2023 上海应用技术大学 上应小风筝团队
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, either version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | use anyhow::Context;
20 | use chrono::{Duration, Local};
21 | use once_cell::sync::OnceCell;
22 |
23 | pub const SCOPE_PUBLIC: u8 = 0;
24 | pub const SCOPE_BALANCE: u8 = 1;
25 |
26 | // TODO: Consider OS compatability
27 | const SLED_CACHE_PATH: &'static str = "./.runtime-cache/";
28 |
29 | static CACHE: OnceCell = OnceCell::new();
30 |
31 | trait CacheItemOperation {
32 | fn is_expired(&self) -> bool;
33 | }
34 |
35 | pub trait CacheOperation
36 | where
37 | T: bincode::Encode + bincode::Decode,
38 | {
39 | fn get(&self, key: &[u8], timeout: Duration) -> anyhow::Result