本项目遵循 Go 社区标准工程结构,并结合 embed 特性实现了前后端一体化打包。注意:文件命名已简化,去除了 _service 或 _handler 后缀。
easy-qfnu-api-go/
├── api/ # 【接口层】Handler 处理函数
│ └── v1/ # API 版本控制
│ └── grade.go # 成绩相关接口 (原 grade_handler.go)
├── common/ # 【公共层】通用工具
│ ├── logger/ # 日志配置 (slog + lumberjack)
│ └── response/ # 统一响应封装 (泛型支持)
├── middleware/ # 【中间件】全局拦截器 (CORS, Auth, Logger)
├── model/ # 【模型层】数据结构定义 (Struct)
│ └── grade.go # 成绩结构体 & 请求参数定义
├── service/ # 【逻辑层】业务逻辑、爬虫、解析
│ ├── client.go # 统一的 Resty 客户端工厂 (含 upstream 拦截)
│ └── grade.go # 成绩业务逻辑 (原 grade_service.go)
├── web/ # 【前端源码】(编译时嵌入二进制)
│ ├── templates/ # HTML 模板
│ │ ├── layouts/ # 布局 (base.html)
│ │ └── grade.html # 功能页
│ └── static/ # 静态资源
│ ├── js/
│ │ ├── api/ # API 封装 (request.js, grade.js)
│ │ └── pages/ # 页面逻辑
│ └── css/ # 自定义样式
├── logs/ # 【运行时】日志文件目录 (自动生成)
├── go.mod # 依赖管理
└── main.go # 程序入口 (路由注册、静态资源挂载)
-
传输协议: HTTP/1.1
-
请求方式:
-
查询数据统一使用
GET(参数放 Query String)。 -
提交操作使用
POST(参数放 JSON Body)。 -
响应格式: 统一使用
application/json; charset=utf-8。
本系统采用 Header 鉴权。
| 参数名 | 位置 | 必填 | 说明 | 示例 |
|---|---|---|---|---|
| Authorization | Header | 是 | 教务系统的完整 Cookie 字符串 | JSESSIONID=... |
后端已启用 middleware.AuthRequired。
- 如果请求未携带
Authorization,接口将直接返回401状态码,业务逻辑不会执行。 - Handler 写法: 不需要手动判断 Authorization 是否为空,直接从 Context 获取:
Authorization := c.GetString("Authorization")使用了 Go 泛型 (Generics) 进行封装。
// common/response/response.go
type Response[T any] struct {
Code int `json:"code"` // 0:成功, 非0:错误码
Msg string `json:"msg"` // 提示信息
Data T `json:"data"` // 泛型数据
}// 成功 (自动推导类型)
response.Success(c, data)
// 失败 (显式指定 any)
response.FailWithCode[any](c, response.CodeAuthExpired, "Cookie 已失效")本项目使用 log/slog 标准库配合 lumberjack 进行日志管理。
-
初始化: 在
main.go启动时调用logger.InitLogger。 -
输出策略:
-
控制台: 彩色文本 (Tint),方便开发调试。
-
文件: JSON 格式,按大小切割,保留历史备份。
-
文件命名:
logs/easy-qfnu-api-{时间戳}.log(每次启动独立文件)。
使用示例:
import "log/slog"
slog.Info("抓取成绩成功", "term", "2024-1", "count", 10)
slog.Error("解析失败", "err", err)新增功能(如:查询课表)步骤:
- Model:
model/schedule.go(定义结构体 &ScheduleRequest)。 - Service:
service/schedule.go(定义FetchSchedule,使用NewJwcClient创建请求)。 - API:
api/v1/schedule.go(定义GetSchedule,绑定参数,调用 Service)。 - Router: 在
main.go的apiGroup中注册路由。
| 技术 | 说明 | 引入方式 |
|---|---|---|
| TailwindCSS | CSS 框架 | CLI 编译,输出到 /static/css/tailwind.css |
| Alpine.js | 轻量级响应式框架 | <script defer src="https://unpkg.com/alpinejs@3/dist/cdn.min.js"></script> |
| Axios | HTTP 请求库 | <script src="https://unpkg.com/axios/dist/axios.min.js"></script> |
| Go Template | 服务端模板渲染 | 内置 |
# 安装 TailwindCSS CLI
npm install -D tailwindcss
# 初始化配置文件
npx tailwindcss init
# 开发模式(监听文件变化)
npx tailwindcss -i ./web/static/css/input.css -o ./web/static/css/tailwind.css --watch
# 生产模式(压缩输出)
npx tailwindcss -i ./web/static/css/input.css -o ./web/static/css/tailwind.css --minify为了保护用户隐私(截图分享时),Cookie 输入框必须实现 “自动模糊,聚焦清晰” 的效果。
HTML 实现 (Tailwind 类名):
<textarea
class="... transition-all duration-300 blur-[4px] focus:blur-none hover:blur-none opacity-60 focus:opacity-100 ..."></textarea>严禁在响应拦截器中直接写 window.location.href = '/login',这会导致死循环刷新。
正确逻辑:
service.interceptors.response.use((response) => {
const res = response.data;
if (res.code !== 0) {
// 1. 优先弹窗提示
vant.showToast(res.msg);
// 2. 特殊处理 Cookie 失效 (401)
if (res.code === 401) {
localStorage.removeItem("qfnu_cookie");
// 可选:仅在非登录页时才跳转,或只提示不跳转
// if (window.location.pathname !== '/login') { ... }
}
return Promise.reject(new Error(res.msg));
}
return res.data; // 直接返回 data 字段
});前端资源通过 embed 打包进 Go 二进制文件。
- HTML 模板: 使用
r.LoadHTMLGlob加载。 - 静态资源: 使用
http.FS配合r.NoRoute托管,解决 SPA 路由刷新 404 问题。
请维护 common/response/code.go。
| 常量名 | Code | 说明 |
|---|---|---|
CodeSuccess |
0 | 成功 |
CodeServerBusy |
1 | 通用错误/系统繁忙 |
CodeInvalidParam |
1001 | 参数绑定失败 |
CodeAuthExpired |
401 | Authorization 过期/未登录 |
CodeTargetError |
502 | 教务系统无响应 |
-
文件名: 全小写,下划线分隔,不带后缀。
-
✅
service/grade.go -
❌
service/grade_service.go -
Go 变量: 驼峰命名 (
gradeList)。 -
JSON 字段: 蛇形命名 (
course_name)。 -
前端 API:
window.GradeApi(挂载在 window 对象上,方便模板调用)。
由于使用了 embed,本项目编译后为单文件部署。
- 编译:
go build -o qfnu-app.exe - 运行: 直接运行
.exe文件即可,无需携带web目录。 - 日志: 程序会自动在运行目录下生成
logs文件夹。