基于 Go + Gin 的模块化 API 服务
各功能以独立模块形式挂载到 /api/<模块名> 路由组下,共享数据库、Redis 缓存与 CookieCloud Cookie 管理
作为个人使用项目,重构实现了部分自己有需求的api接口,感谢原作者的付出❤️
| 模块 | 路由前缀 | 功能 | 外部依赖 |
|---|---|---|---|
| bili_info | /api/bili_info |
代理 Bilibili 视频信息接口 | CookieCloud(可选) |
| qqmail_head | /api/qqmail_head |
获取 QQ 邮箱头像 | CookieCloud(必需) |
| star_vote | /api/star_vote |
投票与评分 | 数据库 |
| gen_line_animation | /api/gen_line_animation |
生成手写签名动画 SVG | 无 |
| site_info | /api/site_info |
抓取网页标题/描述/图标 | Redis(可选) |
| path_proxy | /api/path_proxy |
按路径规则反向代理到目标上游 | 无 |
| star_history | /api/star_history |
生成 GitHub 星标历史图表 SVG | GitHub Token(需仓库管理员/协作者) |
模块默认禁用,可通过 modules.<name>.enable: true 开启(见配置)。
从Releases下载对应架构的发行版及仓库内的config.example.yaml
重命名config.example.yaml为config.yaml,按需调整相关配置
完整配置项见 config.example.yaml
- Go 1.25+(见 go.mod)
go run ./cmd/server
# 或构建二进制(未注入版本号时 /ping 返回 "dev")
go build -ldflags="-s -w -X github.com/thun888/apibox/internal/api.Version=v1.2.3" -o apibox ./cmd/servercurl http://localhost:8080/ping
# {"message":"pong","version":"dev"}
curl "http://localhost:8080/api/bili_info/get_video_info?bvid=BV1xx411U7xx" \
-H "Referer: http://localhost:4000/"点击展开
除特别说明外,模块接口均要求 Referer 头命中该模块的 allowed_referers 白名单,否则返回 403。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /ping |
健康检查,返回 {"message":"pong","version":"<版本>"} |
| GET | / |
302 重定向到本仓库 |
GET /api/bili_info/get_video_info
| 参数 | 必填 | 说明 |
|---|---|---|
| bvid | 是 | 视频 BV 号 |
- 请求
https://api.bilibili.com/x/web-interface/view,响应体原样透传。 - 配置了 Redis 时缓存 1 小时(key:
video:bvid:<bvid>)。 - 配置了 CookieCloud 时附带
.bilibili.com域 Cookie;获取失败仅记录警告,继续请求。 - 错误响应:400
{"error":"missing bvid"}(缺少 bvid);502{"error":"upstream error"}(上游请求失败)。
GET /api/qqmail_head/:email,返回头像图片(Content-Type 来自上游,缺省 image/png)。
- 响应头
Cache-Control: public, max-age=2592000(30 天)。 - 依赖 CookieCloud 提供
.mail.qq.com域 Cookie,获取失败返回 500。 - 错误响应:400
{"error":"Invalid email format"};500{"error":"Failed to get cookies"};502 上游失败(消息中包含上游状态码)。
示例:
curl -o avatar.png "http://localhost:8080/api/qqmail_head/123456@qq.com"数据持久化到数据库(表 starvote_votes、starvote_ratings),使用 UPSERT(ON CONFLICT DO UPDATE)累加计数。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/star_vote/vote/update |
投票 +1:value 为 up/down |
| POST | /api/star_vote/rating/update |
评分 +1:value 为 1–5 的数字(小数向上取整) |
| GET | /api/star_vote/vote/info |
查询投票数 |
| GET | /api/star_vote/rating/info |
查询评分分布 |
- 参数
id可放在表单(application/x-www-form-urlencoded)或查询字符串中,表单优先;GET 接口的id缺省为default。 - 更新成功返回
{"success":"true"};参数缺失或非法返回 400({"code":400,"message":"Bad Request"});数据库错误返回 500。 - 查询不存在的 id 时返回全 0 的结果。
示例:
curl -X POST "http://localhost:8080/api/star_vote/vote/update" \
-H "Referer: http://localhost:4000/" \
-d "id=test&value=up"GET /api/gen_line_animation/signature
| 参数 | 缺省 | 说明 |
|---|---|---|
| name | Signature |
要绘制的文本 |
| animate | false |
为 true 时按笔画顺序播放动画 |
| speed | 1 |
动画速度倍率(须 > 0) |
| color | #000000 |
笔画颜色 |
返回 SVG(image/svg+xml),响应头 Cache-Control: public, max-age=31536000。
/api/path_proxy/<path>,支持任意 HTTP 方法。
按 modules.pathproxy.path_rules 规则把请求转发到目标上游:
path:模块前缀/api/path_proxy之后的路径,支持精确匹配(如/api1/users)与末尾*通配(如/api2/*)。通配命中后,通配部分会拼接到target的路径之后。target:目标上游 URL(http/https),如https://api.example.com/users或https://api.example.com/。allowed_referers:当前规则单独的 Referer 白名单。headers:可选,转发时额外设置或覆盖的上游请求头。- 请求方法、查询参数与请求体原样转发;查询参数会与
target中已有的 query 合并。 - 未命中任何规则返回 404;上游失败返回 502;Referer 不在白名单返回 403。
示例:
curl "http://localhost:8080/api/path_proxy/api1/users?active=1" \
-H "Referer: http://localhost:4000/"移植 site-info-api 两个接口:
| 参数 | 缺省 | 说明 |
|---|---|---|
| url | 必填 | 目标网页 URL(仅 http/https) |
| type | site |
兼容参数,仅接受 site,其余值返回 {} |
| embedIcon | 无 | 为 1/true 时抓取图标转 data URL,填入 iconBase64 |
- 返回
{"title","desc","icon","iconBase64","url"},字段缺省省略;未提取到任何信息或上游失败返回{}。
| 参数 | 缺省 | 说明 |
|---|---|---|
| url | 必填 | 目标网页 URL(仅 http/https) |
- 返回图标文件本体(二进制),Content-Type 来自图标上游,缺省
image/x-icon。 - 错误响应(JSON):400
{"error":"Invalid url"};404{"error":"Icon not found"}(页面未提取到图标或图标上游非 200);500{"error":"Failed to fetch icon"}(图标抓取失败);502{"error":"upstream error"}(页面抓取失败)。
- 图标选择顺序:
rel=apple-touch-icon→rel=icon→og:image→twitter:image→ 其他含 icon 的 link;相对路径按页面最终 URL 解析;data:/javascript:等非 http(s) 结果丢弃。 - 站点信息与图标本体均 Redis 缓存 30 天(图标超过 2MB 不缓存,直接跳转);响应头
Cache-Control: public, max-age=604800(与原版 CDN 头一致)。 - 抓取超时 15s、HTML 上限 2MB、跟随重定向上限 10 次。
- 可选上游代理(
modules.siteinfo.proxy):目标域名命中规则中的domains(精确域名或*.example.com通配、*表示全部,端口不参与匹配)时,页面与图标抓取改走template代理地址,{href}原样替换为目标 URL(不额外编码,适合https://proxy.example.com/{href}风格的前缀代理);重定向目标命中规则时同样改写,图标超限 302 的跳转地址也会改写为代理地址。命中代理后不再校验目标主机(连接由代理发起),但代理地址本身须为公网 http(s)。 - 403(Referer 不在白名单)。
示例:
curl "http://localhost:8080/api/site_info/info?url=https://github.com/thun888/apibox&embedIcon=1" \
-H "Referer: http://localhost:4000/"
curl -o icon.png "http://localhost:8080/api/site_info/icon?url=https://github.com/thun888/apibox" \
-H "Referer: http://localhost:4000/"star-history 的 SVG 生成能力移植,GET /api/star_history/svg。
| 参数 | 缺省 | 说明 |
|---|---|---|
| repos | 必填 | 仓库列表,逗号分隔(最多 20 个),如 thun888/apibox,microsoft/vscode |
| type | date |
X 轴模式:date(绝对日期)/ timeline(相对首日的时长) |
| size | laptop |
图表宽度:mobile(600) / laptop(800) / desktop(1000) |
| theme | light |
dark / light |
| transparent | 无 | 为 true 时背景透明 |
| legend | top-left |
图例位置:top-left / bottom-right |
| logscale | 无 | 只要出现且值不为 false 即启用 Y 轴对数刻度 |
- 返回 SVG(
image/svg+xml;charset=utf-8),响应头Cache-Control: public, s-maxage=86400, max-age=86400。 - 仓库名大小写不敏感:非小写请求会 301 到规范 URL。
- 数据源:GitHub stargazers API(
GET api.github.com/repos/{repo}/stargazers,Accept: application/vnd.github.star+json返回带starred_at的列表,按日期聚合为累计星标数),与原项目流水线的数据语义一致。2026-06-30 起 GitHub 将该 API 限制为仓库管理员/协作者可见,因此需在secrets.github_token配置令牌,且只能生成本人拥有/协作仓库的图表。 - 一次请求最多 20 个仓库,未命中的仓库并发抓取(翻页
per_page=100);头像取仓库 owner 的avatar_url(&s=22)并内联为 base64 data URL。 - 星标数据缓存 24 小时:优先 Redis,未命中读数据库表
starhistory_star_data_caches(配置了数据库时自动启用),仍未命中才请求 GitHub API。回源时复用过期的库缓存行做增量抓取——总数持平只翻最后一页确认(2 次 API 调用即完成刷新),总数增加只翻新增尾页并合并,合并结果与stargazers_count校验不符或总数减少时回退全量;回源结果写回两级缓存。渲染结果(SVG)仅 Redis 缓存。 - 不校验 Referer(SVG 通常内嵌于 README / 卡片场景)。
- 错误响应:400(缺少
repos/ 超过 20 个仓库);404(仓库不存在、无权访问或星标记录少于 5 条,如Repo not found in dataset: xxx/yyy);502(GitHub API 失败、令牌无效或配额耗尽);503(style=landscape1,OG 卡片已禁用)。
示例:
curl -o stars.svg "http://localhost:8080/api/star_history/svg?repos=thun888/apibox&theme=dark"每个模块实现 Controller 接口,通过 init() 自动注册;main 中只需匿名导入 internal/api/modules 汇总包:
type Controller interface {
Register(r *gin.RouterGroup) // 注册子路由;Group 已带 /api/<模块名> 前缀
ModuleName() string // 模块名,即路由前缀
Enabled() bool // 返回 false 时跳过路由注册
}
func RegisterController(c Controller)启动流程(cmd/server/main.go):
- 加载
config.yaml - 初始化数据库(
auto_migrate: true时自动迁移所有已注册模型) - 初始化 Redis(
addr为空则跳过) - 注册全局中间件(CORS、可信代理)与
/、/ping路由 - 遍历已注册模块,按
Enabled()决定是否挂载
模块通过包级变量共享基础设施:
| 组件 | 位置 | 说明 |
|---|---|---|
| 数据库 | database.DB(GORM) |
支持 MySQL/PostgreSQL/SQLite;模型通过 database.RegisterModel 注册 |
| 缓存 | cache.Client(go-redis) |
未配置时为 nil,使用前需判空 |
| 日志 | utils.NewModuleLogger |
slog 文本输出到 stdout,自动带 module 属性 |
| 配置 | config.Cfg |
模块配置在 ModulesConfig,共享凭据在顶层 secrets 段 |
请求进入 gin 路由
→ CORS / 可信代理中间件
→ 模块 handler:Referer 白名单校验(失败 403)
→ 查询 Redis 缓存(命中则直接返回)
→ 带 UA/Referer/Cookie 请求上游 API(超时 10s)
→ 写入缓存(1h)→ 返回
- Referer 白名单:
modules.<name>.allowed_referers,对 Referer 头的主机名做后缀匹配;配置项为"*"时允许任意 Referer 主机;Referer 缺失或不在白名单返回 403。bili_info、star_vote、gen_line_animation、site_info 使用模块级白名单;path_proxy 只在每条path_rules中单独配置,无模块级allowed_referers。qqmail_head、star_history 未启用(前者头像图片、后者 SVG 图表通常内嵌于第三方页面)。 - CORS:
server.allowed_origins。包含"*"时允许任意 Origin(回显请求 Origin,配合 Credentials 不能直接返回*);否则按列表精确匹配。 - 可信代理:
server.trusted_proxies传给 gin 的SetTrustedProxies,影响c.ClientIP()。
internal/utils/cookie.go 实现了从 CookieCloud 服务获取并解密 Cookie 的客户端:
- 接口:
GET {host}/get/{uuid},crypto_type非空且非legacy时作为查询参数。 - 解密算法:
legacy(CryptoJS/OpenSSL 格式,缺省)或aes-128-cbc-fixed。 - 结果按域名过滤(
.bilibili.com、.mail.qq.com)后注入上游请求头,不返回给调用方。 - 配置了 Redis 时,解密结果缓存 1 小时。
- 新建
internal/api/modules/<name>/,实现Controller并在init()中注册:
package newmodule
import (
"github.com/gin-gonic/gin"
"github.com/thun888/apibox/internal/api"
"github.com/thun888/apibox/internal/config"
)
const moduleName = "newmodule"
type Controller struct{}
func init() { api.RegisterController(&Controller{}) }
func (c *Controller) Register(r *gin.RouterGroup) {
r.GET("/hello", func(ctx *gin.Context) { ctx.String(200, "hello") })
}
func (c *Controller) ModuleName() string { return moduleName }
func (c *Controller) Enabled() bool { return config.Cfg.Modules.NewModule.Enabled() }- 在 internal/api/modules/modules.go 中追加匿名导入。
- 需要配置项时,在 internal/config/config.go 的
ModulesConfig中增加字段,并在config.example.yaml中补充示例。 - 需要数据库表时,定义 GORM 模型并在
init()中调用database.RegisterModel(auto_migrate: true时启动自动建表)。表名通过database.BuildTableName加模块前缀:
func (Vote) TableName() string { return database.BuildTableName(&Vote{}, "starvote_") } // → starvote_votes接口返回 403?
Referer 头缺失,或其主机名不在对应模块的 allowed_referers 白名单中(后缀匹配)。qqmail_head、star_history 不校验 Referer。
star_history 返回 404?
2026-06-30 起 GitHub 将 stargazers API 限制为仓库管理员/协作者可见,因此只能生成本人拥有/协作仓库的图表(其他仓库返回 Repo not found in dataset);星标记录少于 5 条的仓库同样返回 404。
如何启用/禁用某个模块?
模块默认禁用,在 config.yaml 中设置 modules.<name>.enable: true 才会注册该模块的路由;设为 false 或删除该字段则不会注册。
qqmail_head 返回 500?
该模块依赖 CookieCloud。确认 cookiecloud 配置里的 host/uuid/password 正确、GET {host}/get/{uuid} 可访问、crypto_type 与服务端一致(legacy 或 aes-128-cbc-fixed)。
配置了 Redis 但启动失败?
启动时会先 ping 一次 Redis,连不上则直接退出。不使用 Redis 时将 redis.addr 留空。