Compare commits

..
2 Commits
Author SHA1 Message Date
chenxin cf16c37f32 ✨ feat(cache): 新增 Redis 缓存与 PostgreSQL 后端支持
- 新增 cache.redis 配置(独立 host/port/password/db 字段),启用后正文缓存与音频代理缓存走 Redis,键前缀 fq:cache:/fq:audio:,TTL 自动过期
- 新增 cache.backend: sqlite|postgres 切换,PG 连接用 net/url 构造 DSN,密码特殊字符自动转义;SQLite 顺手启用 WAL 与 busy_timeout
- SQL 方言收敛:q() 将 ? 占位符转换为 $n、INSERT OR IGNORE 与查询再插入统一为 ON CONFLICT、LastInsertId 改为 RETURNING id、建表 DDL 双版本分派
- PG 下 VACUUM 走 autovacuum 说明、状态页大小改查 pg_database_size
- 修复 PG 下设备保存失败:is_valid 列为 INTEGER,写入点补 boolToInt 转换
- 新增 .env 支持:敏感配置(密码/主机/访问密钥)由环境变量覆盖 config.yaml,.env 进 gitignore,仓库附 .env.example 模板
- 删除死代码 InitDevicePoolTable;新增 rebind_test.go 单测
- 更新 README 与 config.yaml 配置示例,go.mod 新增 go-redis/v9 与 pgx/v5 依赖
2026-09-29 20:22:52 +08:00
chenxin d710d382ee 🐛 fix(server): 增加优雅退出与请求超时保护
- 新增 SIGINT/SIGTERM 信号处理与 http.Server.Shutdown,退出前确保统计数据保存、数据库正常关闭
- 为 http.Server 设置 ReadHeaderTimeout/ReadTimeout/WriteTimeout/IdleTimeout,防止慢客户端长期占用连接
- 全局配置指针改为 atomic.Pointer 并提供 GetAppConfig 访问器,消除配置热更新时的数据竞争,同时删除冗余的 manager.config 字段
- 请求统计九组计数器重构为 map[string]*atomic.Int64,统一 incrementStat/getStatsSnapshot 访问,精简约 40 行样板代码
- go mod tidy 清理 go.mod 与 go.sum 中的冗余依赖条目
2026-09-29 19:47:04 +08:00
22 changed files with 714 additions and 433 deletions

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# 敏感配置模板:复制为 .env 并填入真实值(.env 已被 gitignore,不会提交)
# 环境变量优先于 config.yaml 中的对应配置
# 服务访问密钥(/status、/self-info 等端点的鉴权)
# FQ_SERVER_ACCESS_KEY=your-access-key
# PostgreSQL 连接(覆盖 cache.postgres.*)
FQ_PG_HOST=127.0.0.1
FQ_PG_USER=postgres
FQ_PG_PASSWORD=
FQ_PG_DATABASE=fanqie
# Redis 连接(覆盖 cache.redis.*)
FQ_REDIS_HOST=127.0.0.1
FQ_REDIS_PASSWORD=
+5
View File
@@ -44,3 +44,8 @@ __pycache__
# Build output
/dist/
/build/
cache.db
# 环境变量(含真实密码,勿提交)
.env
+52 -3
View File
@@ -34,7 +34,7 @@ Go 语言实现的番茄小说非官方 API 服务,完整实现了签名算法
- **签名算法** - Gorgon、8404、Argus、Ladon、Khronos
- **设备密钥池** - 多设备轮换、自动补充
- **SQLite 缓存** - 本地缓存、过期清理、数据库压缩
- **SQLite 缓存** - 本地缓存、过期清理、数据库压缩;支持切换 PostgreSQL 与 Redis
- **配置热更新** - 修改配置无需重启
- **访问控制** - 支持 AccessKey 验证
@@ -266,7 +266,22 @@ fanqie:
# 缓存配置
cache:
enabled: true # 启用缓存
sqlite_path: "./cache.db" # SQLite 路径
backend: sqlite # 存储后端: sqlite | postgres
sqlite_path: "./cache.db" # SQLite 路径(backend 为 sqlite 时使用)
# PostgreSQL连接配置(backend 为 postgres 时使用)
postgres:
host: "127.0.0.1" # 主机
port: 5432 # 端口
user: "postgres" # 用户名
password: "" # 密码
database: "fanqie" # 数据库名
sslmode: "disable" # SSL 模式: disable | prefer | require
redis: # Redis 缓存(正文缓存与音频代理缓存,统计数据/设备池仍在 SQL 库)
enabled: false
host: "127.0.0.1" # 主机
port: 6379 # 端口
password: "" # 密码
db: 0 # 数据库编号
cleanup_enabled: true # 启用自动清理
cleanup_seconds: 86400 # 清理间隔(秒)
@@ -278,6 +293,28 @@ device_key_pool:
max_size: 30 # 最大设备数
```
### 敏感配置(.env)
密码等敏感值不建议写进 `config.yaml`(会进 git 历史)。复制 `.env.example` 为 `.env` 填入真实值即可,`.env` 已被 gitignore:
```bash
cp .env.example .env
```
支持的环境变量(优先于 `config.yaml` 对应项):
| 环境变量 | 覆盖配置 | 说明 |
| -------- | -------- | ---- |
| `FQ_SERVER_ACCESS_KEY` | `server.access_key` | 服务访问密钥 |
| `FQ_PG_HOST` | `cache.postgres.host` | PostgreSQL 主机 |
| `FQ_PG_USER` | `cache.postgres.user` | PostgreSQL 用户名 |
| `FQ_PG_PASSWORD` | `cache.postgres.password` | PostgreSQL 密码 |
| `FQ_PG_DATABASE` | `cache.postgres.database` | PostgreSQL 数据库名 |
| `FQ_REDIS_HOST` | `cache.redis.host` | Redis 主机 |
| `FQ_REDIS_PASSWORD` | `cache.redis.password` | Redis 密码 |
已有环境变量优先于 `.env` 文件中的值。
### 配置说明表
| 配置项 | 说明 | 默认值 |
@@ -289,8 +326,20 @@ device_key_pool:
| `fanqie.signature` | 签名方法 `sign2` / `sign4` | `sign4` |
| `fanqie.content.*` | 正文内容处理选项 | 见上文 |
| `fanqie.manga.output_mode` | 漫画输出模式 | `url` |
| `cache.enabled` | 是否启用 SQLite 缓存 | `true` |
| `cache.enabled` | 是否启用正文内容缓存 | `true` |
| `cache.backend` | SQL 存储后端 `sqlite` / `postgres` | `sqlite` |
| `cache.sqlite_path` | SQLite 数据库路径 | `./cache.db` |
| `cache.postgres.host` | PostgreSQL 主机 | `127.0.0.1` |
| `cache.postgres.port` | PostgreSQL 端口 | `5432` |
| `cache.postgres.user` | PostgreSQL 用户名 | `postgres` |
| `cache.postgres.password` | PostgreSQL 密码 | 空 |
| `cache.postgres.database` | PostgreSQL 数据库名 | `fanqie` |
| `cache.postgres.sslmode` | SSL 模式 | `disable` |
| `cache.redis.enabled` | 启用 Redis 缓存(正文与音频代理缓存走 Redis) | `false` |
| `cache.redis.host` | Redis 主机 | `127.0.0.1` |
| `cache.redis.port` | Redis 端口 | `6379` |
| `cache.redis.password` | Redis 密码 | 空 |
| `cache.redis.db` | Redis 数据库编号 | `0` |
| `device_key_pool.enabled` | 是否启用设备密钥池 | `true` |
| `device_key_pool.initial_size` | 初始设备数量 | `20` |
| `device_key_pool.min_size` | 最小设备数量 | `10` |
+46 -12
View File
@@ -1,6 +1,7 @@
package main
import (
"context"
"fanqie/internal/algos"
"fanqie/internal/common"
"fanqie/internal/config"
@@ -10,6 +11,9 @@ import (
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
var (
@@ -30,6 +34,13 @@ func main() {
}
defer fqapi.CloseCache() // 程序退出时关闭数据库
// 初始化 Redis 缓存(可选)
if err := fqapi.InitRedis(); err != nil {
logger.Error("Redis 初始化失败: " + err.Error())
os.Exit(1)
}
defer fqapi.CloseRedis()
// 加载统计数据
handlers.LoadStatistics()
@@ -85,15 +96,15 @@ func initConfig() {
// initAlgorithm 初始化算法
func initAlgorithm() {
if config.AppConfig != nil {
algos.GlobalAlgorithmSwitcher.SwitchAlgorithm(config.AppConfig.Fanqie.Algorithm)
logger.Info(fmt.Sprintf("当前算法: %s", config.AppConfig.Fanqie.Algorithm))
if config.GetAppConfig() != nil {
algos.GlobalAlgorithmSwitcher.SwitchAlgorithm(config.GetAppConfig().Fanqie.Algorithm)
logger.Info(fmt.Sprintf("当前算法: %s", config.GetAppConfig().Fanqie.Algorithm))
algos.GlobalSignatureSwitcher.SwitchSignature(config.AppConfig.Fanqie.Signature)
algos.GlobalSignatureSwitcher.SwitchSignature(config.GetAppConfig().Fanqie.Signature)
// 根据签名方法显示详细信息
var signatureDetails string
if config.AppConfig.Fanqie.Signature == "sign4" {
if config.GetAppConfig().Fanqie.Signature == "sign4" {
signatureDetails = "X-Gorgon + X-Ladon + X-Argus + X-Khronos"
} else {
signatureDetails = "X-Gorgon + X-Khronos"
@@ -159,22 +170,45 @@ func startServer() {
host := "0.0.0.0"
port := "40180"
if config.AppConfig != nil {
if config.AppConfig.Server.Host != "" {
host = config.AppConfig.Server.Host
if config.GetAppConfig() != nil {
if config.GetAppConfig().Server.Host != "" {
host = config.GetAppConfig().Server.Host
}
if config.AppConfig.Server.Port != "" {
port = config.AppConfig.Server.Port
if config.GetAppConfig().Server.Port != "" {
port = config.GetAppConfig().Server.Port
}
}
addr := host + ":" + port
logger.Info(fmt.Sprintf("服务器启动在 %s", addr))
server := &http.Server{
Addr: addr,
// 超时保护:防止慢客户端长期占用连接
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 60 * time.Second,
WriteTimeout: 10 * time.Minute, // 代理响应整体缓冲后写出,慢客户端下载大音频/图片需要余量
IdleTimeout: 120 * time.Second,
}
if err := http.ListenAndServe(addr, nil); err != nil {
go func() {
logger.Info(fmt.Sprintf("服务器启动在 %s", addr))
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error(fmt.Sprintf("服务器启动失败: %v", err))
os.Exit(1)
}
}()
// 等待退出信号,优雅关闭,让 main 中的 defer(保存统计、关闭数据库)得以执行
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
<-sigCh
logger.Info("收到退出信号,正在关闭服务器...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Warning(fmt.Sprintf("服务器关闭超时(30秒内未完成的连接被强制断开): %v", err))
}
logger.Info("服务器已关闭")
}
func init() {
+22 -2
View File
@@ -57,9 +57,30 @@ cache:
# 是否启用正文内容缓存
enabled: true
# SQLite数据库文件路径
# 存储后端:sqlite | postgres
backend: postgres
# SQLite数据库文件路径(backend 为 sqlite 时使用)
sqlite_path: "./cache.db"
# PostgreSQL连接配置(backend 为 postgres 时使用)
# 敏感值(密码等)建议放 .env,见 .env.example;环境变量优先于此处配置
postgres:
host: "127.0.0.1"
port: 5432
user: "postgres"
password: ""
database: "fanqie"
sslmode: "disable" # disable | prefer | require
# Redis缓存(启用后正文缓存与音频代理缓存走 Redis,统计数据与设备池仍在 SQL 库)
redis:
enabled: true
host: "127.0.0.1"
port: 6379
password: ""
db: 1
# 是否启用正文内容缓存自动清理
cleanup_enabled: true
@@ -79,4 +100,3 @@ device_key_pool:
# 最大设备数量
max_size: 30
+9 -31
View File
@@ -1,41 +1,28 @@
module fanqie
go 1.24.0
go 1.25.0
require (
github.com/PuerkitoBio/goquery v1.8.1
github.com/fsnotify/fsnotify v1.7.0
github.com/google/uuid v1.4.0
github.com/jackc/pgx/v5 v5.11.0
github.com/mattn/go-sqlite3 v1.14.18
github.com/redis/go-redis/v9 v9.22.0
github.com/spf13/viper v1.18.2
)
require (
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.11.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.55.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -43,22 +30,13 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+28 -146
View File
@@ -1,105 +1,54 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
@@ -117,82 +66,42 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -203,44 +112,17 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+60 -20
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"log"
"sync"
"sync/atomic"
"github.com/spf13/viper"
)
@@ -51,11 +52,33 @@ type MangaConfig struct {
// CacheConfig 缓存配置
type CacheConfig struct {
Enabled bool `mapstructure:"enabled"`
Backend string `mapstructure:"backend"` // 存储后端: sqlite | postgres
SQLitePath string `mapstructure:"sqlite_path"`
Postgres PostgresConfig `mapstructure:"postgres"`
Redis RedisConfig `mapstructure:"redis"`
CleanupEnabled bool `mapstructure:"cleanup_enabled"` // 是否启用缓存清理
CleanupSeconds int `mapstructure:"cleanup_seconds"` // 缓存清理时间(秒)
}
// PostgresConfig PostgreSQL 连接配置
type PostgresConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
SSLMode string `mapstructure:"sslmode"` // disable | prefer | require
}
// RedisConfig Redis 缓存配置(启用后正文缓存与音频代理缓存走 Redis,统计数据与设备池仍在 SQL 库)
type RedisConfig struct {
Enabled bool `mapstructure:"enabled"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
// DeviceKeyPoolConfig 设备密钥池配置
type DeviceKeyPoolConfig struct {
Enabled bool `mapstructure:"enabled"` // 是否启用设备密钥池
@@ -67,17 +90,21 @@ type DeviceKeyPoolConfig struct {
// ConfigManager 配置管理器
type ConfigManager struct {
mu sync.RWMutex
config *Config
configPath string
callbacks []func(*Config)
}
var (
// AppConfig 全局配置实例
AppConfig *Config
// appConfig 全局配置实例(原子指针,热更新替换时读取侧无数据竞争)
appConfig atomic.Pointer[Config]
manager *ConfigManager
)
// GetAppConfig 获取当前配置,未加载时返回 nil
func GetAppConfig() *Config {
return appConfig.Load()
}
// NewConfigManager 创建配置管理器
func NewConfigManager() *ConfigManager {
return &ConfigManager{
@@ -93,6 +120,9 @@ func LoadConfig(configPath string) error {
manager.configPath = configPath
}
// 加载 .env(若存在),敏感配置可由环境变量覆盖
loadDotenv("./.env")
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
@@ -107,12 +137,10 @@ func LoadConfig(configPath string) error {
if err := viper.Unmarshal(newConfig); err != nil {
return fmt.Errorf("解析配置文件失败: %w", err)
}
applyEnvOverrides(newConfig)
// 更新配置
manager.mu.Lock()
manager.config = newConfig
AppConfig = newConfig
manager.mu.Unlock()
appConfig.Store(newConfig)
return nil
}
@@ -142,10 +170,26 @@ func setDefaults() {
// 缓存配置
viper.SetDefault("cache.enabled", true)
viper.SetDefault("cache.backend", "sqlite")
viper.SetDefault("cache.sqlite_path", "./cache.db")
viper.SetDefault("cache.cleanup_enabled", false)
viper.SetDefault("cache.cleanup_seconds", 86400)
// PostgreSQL 配置
viper.SetDefault("cache.postgres.host", "127.0.0.1")
viper.SetDefault("cache.postgres.port", 5432)
viper.SetDefault("cache.postgres.user", "postgres")
viper.SetDefault("cache.postgres.password", "")
viper.SetDefault("cache.postgres.database", "fanqie")
viper.SetDefault("cache.postgres.sslmode", "disable")
// Redis 配置
viper.SetDefault("cache.redis.enabled", false)
viper.SetDefault("cache.redis.host", "127.0.0.1")
viper.SetDefault("cache.redis.port", 6379)
viper.SetDefault("cache.redis.password", "")
viper.SetDefault("cache.redis.db", 0)
// 设备密钥池配置
viper.SetDefault("device_key_pool.enabled", true)
viper.SetDefault("device_key_pool.initial_size", 3)
@@ -155,18 +199,11 @@ func setDefaults() {
// GetConfig 获取配置实例
func GetConfig() *Config {
if manager != nil {
manager.mu.RLock()
defer manager.mu.RUnlock()
if manager.config != nil {
return manager.config
if cfg := appConfig.Load(); cfg != nil {
return cfg
}
}
if AppConfig == nil {
log.Fatal("配置未初始化,请先调用 LoadConfig")
}
return AppConfig
return nil
}
// ReloadConfig 重新加载配置文件
@@ -193,12 +230,15 @@ func ReloadConfig() error {
if err := viper.Unmarshal(newConfig); err != nil {
return fmt.Errorf("重新解析配置文件失败: %w", err)
}
applyEnvOverrides(newConfig)
// 更新配置
manager.mu.Lock()
oldAlgorithm := manager.config.Fanqie.Algorithm
manager.config = newConfig
AppConfig = newConfig
var oldAlgorithm string
if old := appConfig.Load(); old != nil {
oldAlgorithm = old.Fanqie.Algorithm
}
appConfig.Store(newConfig)
callbacks := make([]func(*Config), len(manager.callbacks))
copy(callbacks, manager.callbacks)
manager.mu.Unlock()
+49
View File
@@ -0,0 +1,49 @@
package config
import (
"os"
"strings"
)
// loadDotenv 加载 .env 文件到环境变量(文件不存在则跳过;不覆盖已有环境变量)
func loadDotenv(path string) {
data, err := os.ReadFile(path)
if err != nil {
return
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
k = strings.TrimSpace(k)
v = strings.Trim(strings.TrimSpace(v), `"'`)
if os.Getenv(k) == "" {
os.Setenv(k, v)
}
}
}
// applyEnvOverrides 用环境变量覆盖敏感配置(配合 .env 使用,真实密码不进 config.yaml)
func applyEnvOverrides(c *Config) {
for _, o := range []struct {
env string
dst *string
}{
{"FQ_SERVER_ACCESS_KEY", &c.Server.AccessKey},
{"FQ_PG_HOST", &c.Cache.Postgres.Host},
{"FQ_PG_USER", &c.Cache.Postgres.User},
{"FQ_PG_PASSWORD", &c.Cache.Postgres.Password},
{"FQ_PG_DATABASE", &c.Cache.Postgres.Database},
{"FQ_REDIS_HOST", &c.Cache.Redis.Host},
{"FQ_REDIS_PASSWORD", &c.Cache.Redis.Password},
} {
if v := os.Getenv(o.env); v != "" {
*o.dst = v
}
}
}
+33 -8
View File
@@ -20,6 +20,16 @@ func GenerateAudioProxyURL(originalURL, deviceID string, headers map[string]stri
// 生成缓存key
key := generateRandomKey(16)
// Redis 模式:TTL 自动过期
if redisCache != nil {
saveAudioProxyEntry(key, &audioProxyEntry{
OriginalURL: originalURL,
DeviceID: deviceID,
Headers: headers,
})
return fmt.Sprintf("/audio/proxy/%s", key)
}
// 将headers序列化为JSON字符串
headersJSON, _ := json.Marshal(headers)
@@ -28,10 +38,10 @@ func GenerateAudioProxyURL(originalURL, deviceID string, headers map[string]stri
cacheMutex.Lock()
defer cacheMutex.Unlock()
_, err := cacheDB.Exec(`
_, err := cacheDB.Exec(q(`
INSERT INTO audio_cache (key, original_url, device_id, headers, created_at)
VALUES (?, ?, ?, ?, ?)
`, key, originalURL, deviceID, string(headersJSON), time.Now().Unix())
`), key, originalURL, deviceID, string(headersJSON), time.Now().Unix())
if err != nil {
audioLogger.Error("保存音频缓存失败: " + err.Error())
}
@@ -43,19 +53,29 @@ func GenerateAudioProxyURL(originalURL, deviceID string, headers map[string]stri
// HandleAudioProxy 处理音频代理请求
func HandleAudioProxy(key string) ([]byte, string, error) {
var originalURL, deviceID string
var headers map[string]string
if redisCache != nil {
entry, err := loadAudioProxyEntry(key)
if err != nil {
return nil, "", err
}
originalURL, deviceID, headers = entry.OriginalURL, entry.DeviceID, entry.Headers
} else {
if cacheDB == nil {
return nil, "", fmt.Errorf("数据库未初始化")
}
// 从数据库获取音频信息
var originalURL, deviceID, headersJSON string
var headersJSON string
var createdAt int64
cacheMutex.Lock()
err := cacheDB.QueryRow(`
err := cacheDB.QueryRow(q(`
SELECT original_url, device_id, headers, created_at
FROM audio_cache WHERE key = ?
`, key).Scan(&originalURL, &deviceID, &headersJSON, &createdAt)
`), key).Scan(&originalURL, &deviceID, &headersJSON, &createdAt)
cacheMutex.Unlock()
if err == sql.ErrNoRows {
@@ -69,14 +89,14 @@ func HandleAudioProxy(key string) ([]byte, string, error) {
if time.Now().Unix()-createdAt > 7200 { // 2小时 = 7200秒
// 清理过期缓存
cacheMutex.Lock()
cacheDB.Exec("DELETE FROM audio_cache WHERE key = ?", key)
cacheDB.Exec(q("DELETE FROM audio_cache WHERE key = ?"), key)
cacheMutex.Unlock()
return nil, "", fmt.Errorf("音频信息已过期,请重新调用 /audio 接口")
}
// 解析headers
var headers map[string]string
json.Unmarshal([]byte(headersJSON), &headers)
}
if headers == nil {
headers = make(map[string]string)
}
@@ -92,6 +112,11 @@ func HandleAudioProxy(key string) ([]byte, string, error) {
// CleanExpiredAudioCache 清理过期的音频缓存
func CleanExpiredAudioCache() {
// Redis 模式下由 TTL 自动过期,无需清理
if redisCache != nil {
return
}
if cacheDB == nil {
return
}
@@ -100,7 +125,7 @@ func CleanExpiredAudioCache() {
defer cacheMutex.Unlock()
// 删除2小时前的缓存
result, err := cacheDB.Exec("DELETE FROM audio_cache WHERE created_at < ?", time.Now().Unix()-7200)
result, err := cacheDB.Exec(q("DELETE FROM audio_cache WHERE created_at < ?"), time.Now().Unix()-7200)
if err != nil {
audioLogger.Error("清理过期音频缓存失败: " + err.Error())
return
+217 -75
View File
@@ -1,19 +1,27 @@
package fqapi
import (
"context"
"database/sql"
"encoding/json"
"fanqie/internal/common"
"fanqie/internal/config"
"fmt"
"net/url"
"strconv"
"strings"
"sync"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
_ "github.com/mattn/go-sqlite3"
"github.com/redis/go-redis/v9"
)
var (
cacheDB *sql.DB
dbDriver string // "sqlite3" | "pgx"
redisCache *redis.Client
cacheMutex sync.Mutex
cacheLogger = common.NewLogger("cache_manager", "INFO", "")
contentRequestCount int // content接口访问计数
@@ -21,46 +29,137 @@ var (
cacheCleanupTicker *time.Ticker // 缓存清理定时器
)
// q 按数据库方言转换占位符(PostgreSQL 使用 $n 形式)
func q(query string) string {
if dbDriver != "pgx" {
return query
}
var b strings.Builder
n := 0
for i := 0; i < len(query); i++ {
if query[i] == '?' {
n++
b.WriteString("$" + strconv.Itoa(n))
} else {
b.WriteByte(query[i])
}
}
return b.String()
}
// InitRedis 初始化 Redis 缓存(启用后正文缓存与音频代理缓存走 Redis)
func InitRedis() error {
cfg := config.GetConfig()
if cfg == nil || !cfg.Cache.Redis.Enabled {
return nil
}
redisCache = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Cache.Redis.Host, cfg.Cache.Redis.Port),
Password: cfg.Cache.Redis.Password,
DB: cfg.Cache.Redis.DB,
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := redisCache.Ping(ctx).Err(); err != nil {
redisCache = nil
return fmt.Errorf("连接 Redis 失败: %w", err)
}
cacheLogger.Info(fmt.Sprintf("Redis 缓存已启用: %s:%d db=%d", cfg.Cache.Redis.Host, cfg.Cache.Redis.Port, cfg.Cache.Redis.DB))
return nil
}
// CloseRedis 关闭 Redis 连接
func CloseRedis() {
if redisCache != nil {
redisCache.Close()
}
}
// CacheData 缓存数据结构
type CacheData struct {
Data string `json:"data"`
Expi float64 `json:"expi"`
}
// InitDB 初始化数据库
// InitDB 初始化数据库(按 cache.backend 选择 sqlite 或 postgres)
func InitDB() error {
if cacheDB != nil {
return nil // 已初始化
}
// 使用配置的路径
dbPath := "./cache.db"
cfg := config.GetConfig()
if cfg != nil && cfg.Cache.SQLitePath != "" {
dbPath = cfg.Cache.SQLitePath
backend := "sqlite"
dsn := "./cache.db"
if cfg != nil {
if cfg.Cache.Backend != "" {
backend = cfg.Cache.Backend
}
if backend == "postgres" {
pg := cfg.Cache.Postgres
if pg.Host == "" || pg.Database == "" {
return fmt.Errorf("cache.backend 为 postgres 但未配置 host/database")
}
// 用 net/url 构造连接串,密码中的特殊字符会被自动转义
u := &url.URL{
Scheme: "postgres",
User: url.UserPassword(pg.User, pg.Password),
Host: fmt.Sprintf("%s:%d", pg.Host, pg.Port),
Path: pg.Database,
RawQuery: "sslmode=" + pg.SSLMode,
}
dsn = u.String()
} else if cfg.Cache.SQLitePath != "" {
dsn = cfg.Cache.SQLitePath
}
}
switch backend {
case "postgres":
dbDriver = "pgx"
default:
dbDriver = "sqlite3"
// ponytail: 单文件场景,WAL + 忙等待即可,无需更多调优
dsn = dsn + "?_journal_mode=WAL&_busy_timeout=5000"
}
var err error
cacheDB, err = sql.Open("sqlite3", dbPath)
cacheDB, err = sql.Open(dbDriver, dsn)
if err != nil {
cacheLogger.Error("打开数据库失败: " + err.Error())
return err
}
if err := cacheDB.Ping(); err != nil {
cacheLogger.Error("连接数据库失败: " + err.Error())
return err
}
cacheLogger.Info(fmt.Sprintf("数据库已连接: %s (driver=%s)", backend, dbDriver))
// 创建缓存表(添加created_at字段用于清理)
_, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS cache (
if err := createTables(); err != nil {
return err
}
return nil
}
// createTables 创建数据表(两套 DDL 各自兼容对应方言)
func createTables() error {
// 通用部分:statistics 初始化、索引、audio_cache 建表在两方言间一致或按 q() 转换
tables := []struct{ name, sqliteDDL, pgDDL string }{
{"cache",
`CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY,
value TEXT,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)`)
if err != nil {
cacheLogger.Error("创建缓存表失败: " + err.Error())
return err
}
// 创建statistics表(保存请求统计,始终使用)
_, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS statistics (
)`,
`CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY,
value TEXT,
created_at INTEGER DEFAULT 0
)`},
{"statistics",
`CREATE TABLE IF NOT EXISTS statistics (
id INTEGER PRIMARY KEY CHECK (id = 1),
total_requests INTEGER DEFAULT 0,
search_requests INTEGER DEFAULT 0,
@@ -72,26 +171,22 @@ func InitDB() error {
video_requests INTEGER DEFAULT 0,
category_requests INTEGER DEFAULT 0,
updated_at INTEGER DEFAULT (strftime('%s', 'now'))
)`)
if err != nil {
cacheLogger.Error("创建statistics表失败: " + err.Error())
return err
}
// 初始化statistics表(如果不存在记录)
_, err = cacheDB.Exec(`INSERT OR IGNORE INTO statistics (id) VALUES (1)`)
if err != nil {
cacheLogger.Error("初始化statistics表失败: " + err.Error())
}
// 创建索引以提高清理效率
_, err = cacheDB.Exec(`CREATE INDEX IF NOT EXISTS idx_created_at ON cache(created_at)`)
if err != nil {
cacheLogger.Error("创建索引失败: " + err.Error())
}
// 创建device_pool表(设备密钥池)
_, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS device_pool (
)`,
`CREATE TABLE IF NOT EXISTS statistics (
id INTEGER PRIMARY KEY CHECK (id = 1),
total_requests INTEGER DEFAULT 0,
search_requests INTEGER DEFAULT 0,
detail_requests INTEGER DEFAULT 0,
catalog_requests INTEGER DEFAULT 0,
content_requests INTEGER DEFAULT 0,
manga_requests INTEGER DEFAULT 0,
audio_requests INTEGER DEFAULT 0,
video_requests INTEGER DEFAULT 0,
category_requests INTEGER DEFAULT 0,
updated_at INTEGER DEFAULT 0
)`},
{"device_pool",
`CREATE TABLE IF NOT EXISTS device_pool (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_header TEXT NOT NULL,
key TEXT NOT NULL,
@@ -99,30 +194,56 @@ func InitDB() error {
is_valid INTEGER DEFAULT 1,
created_at INTEGER NOT NULL,
last_used_at INTEGER NOT NULL
)`)
if err != nil {
cacheLogger.Error("创建device_pool表失败: " + err.Error())
return err
}
// 创建device_pool索引
_, err = cacheDB.Exec(`CREATE INDEX IF NOT EXISTS idx_device_pool_valid ON device_pool(is_valid)`)
if err != nil {
cacheLogger.Warning("创建device_pool索引失败: " + err.Error())
}
// 创建audio_cache表(音频代理缓存)
_, err = cacheDB.Exec(`CREATE TABLE IF NOT EXISTS audio_cache (
)`,
`CREATE TABLE IF NOT EXISTS device_pool (
id BIGSERIAL PRIMARY KEY,
device_header TEXT NOT NULL,
key TEXT NOT NULL,
key_ver INTEGER NOT NULL,
is_valid INTEGER DEFAULT 1,
created_at INTEGER NOT NULL,
last_used_at INTEGER NOT NULL
)`},
{"audio_cache",
`CREATE TABLE IF NOT EXISTS audio_cache (
key TEXT PRIMARY KEY,
original_url TEXT NOT NULL,
device_id TEXT NOT NULL,
headers TEXT,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)`)
if err != nil {
cacheLogger.Error("创建audio_cache表失败: " + err.Error())
)`,
`CREATE TABLE IF NOT EXISTS audio_cache (
key TEXT PRIMARY KEY,
original_url TEXT NOT NULL,
device_id TEXT NOT NULL,
headers TEXT,
created_at INTEGER DEFAULT 0
)`},
}
for _, t := range tables {
ddl := t.sqliteDDL
if dbDriver == "pgx" {
ddl = t.pgDDL
}
if _, err := cacheDB.Exec(ddl); err != nil {
cacheLogger.Error("创建" + t.name + "表失败: " + err.Error())
return err
}
}
// 初始化statistics表(如果不存在记录)
if _, err := cacheDB.Exec(`INSERT INTO statistics (id) VALUES (1) ON CONFLICT (id) DO NOTHING`); err != nil {
cacheLogger.Error("初始化statistics表失败: " + err.Error())
}
// 创建索引以提高清理效率
if _, err := cacheDB.Exec(`CREATE INDEX IF NOT EXISTS idx_created_at ON cache(created_at)`); err != nil {
cacheLogger.Error("创建索引失败: " + err.Error())
}
if _, err := cacheDB.Exec(`CREATE INDEX IF NOT EXISTS idx_device_pool_valid ON device_pool(is_valid)`); err != nil {
cacheLogger.Warning("创建device_pool索引失败: " + err.Error())
}
return nil
}
@@ -142,6 +263,10 @@ func GetCache(key string) string {
return ""
}
if redisCache != nil {
return redisGetCache(key)
}
if cacheDB == nil {
return ""
}
@@ -150,7 +275,7 @@ func GetCache(key string) string {
defer cacheMutex.Unlock()
var value string
err := cacheDB.QueryRow("SELECT value FROM cache WHERE key = ?", key).Scan(&value)
err := cacheDB.QueryRow(q("SELECT value FROM cache WHERE key = ?"), key).Scan(&value)
if err != nil {
if err != sql.ErrNoRows {
cacheLogger.Error("查询缓存失败: " + err.Error())
@@ -177,13 +302,16 @@ func GetCache(key string) string {
return ""
}
// UpdateCache 更新缓存
// UpdateCache 更新或插入正文内容缓存
func UpdateCache(key string, value string, expi int) error {
if !isCacheEnabled() {
return nil
}
if redisCache != nil {
return redisUpdateCache(key, value, expi)
}
if cacheDB == nil {
return nil
}
@@ -201,18 +329,9 @@ func UpdateCache(key string, value string, expi int) error {
return err
}
// 检查是否存在
var exists bool
err = cacheDB.QueryRow("SELECT 1 FROM cache WHERE key = ?", key).Scan(&exists)
if err == sql.ErrNoRows {
// 不存在,插入
_, err = cacheDB.Exec("INSERT INTO cache (key, value) VALUES (?, ?)", key, string(jsonData))
} else {
// 存在,更新
_, err = cacheDB.Exec("UPDATE cache SET value = ? WHERE key = ?", string(jsonData), key)
}
// UPSERT:ON CONFLICT 语法 SQLite 3.24+ 与 PostgreSQL 通用
_, err = cacheDB.Exec(q(`INSERT INTO cache (key, value, created_at) VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value`), key, string(jsonData), time.Now().Unix())
if err != nil {
cacheLogger.Error("更新缓存失败: " + err.Error())
return err
@@ -248,7 +367,11 @@ func CleanupAllCache() error {
return err
}
// 智能压缩:仅在空闲空间超过阈值时执行
// 智能压缩:仅在空闲空间超过阈值时执行(仅 SQLite,PG 由 autovacuum 自行管理)
if dbDriver == "pgx" {
return nil
}
var freelistCount int64
err = cacheDB.QueryRow("PRAGMA freelist_count").Scan(&freelistCount)
if err == nil && freelistCount > 0 {
@@ -304,7 +427,7 @@ func CleanupExpiredCache() error {
expiredTime := time.Now().Unix() - int64(cfg.Cache.CleanupSeconds)
// 删除过期的缓存(created_at < expiredTime)
result, err := cacheDB.Exec("DELETE FROM cache WHERE created_at < ?", expiredTime)
result, err := cacheDB.Exec(q("DELETE FROM cache WHERE created_at < ?"), expiredTime)
if err != nil {
cacheLogger.Error("清理过期缓存失败: " + err.Error())
return err
@@ -396,6 +519,10 @@ func VacuumDatabase() error {
defer cacheMutex.Unlock()
cacheLogger.Info("开始执行数据库压缩...")
if dbDriver == "pgx" {
cacheLogger.Info("PostgreSQL 由 autovacuum 自动管理,无需手动压缩")
return nil
}
_, err := cacheDB.Exec("VACUUM")
if err != nil {
cacheLogger.Error("数据库压缩失败: " + err.Error())
@@ -426,7 +553,21 @@ func GetDatabaseStats() (map[string]interface{}, error) {
}
stats["cache_count"] = cacheCount
// 获取数据库页面统计
// PostgreSQL:用库大小填充,无 freelist 概念
if dbDriver == "pgx" {
var totalSize int64
if err := cacheDB.QueryRow("SELECT pg_database_size(current_database())").Scan(&totalSize); err == nil {
stats["page_count"] = int64(0)
stats["page_size"] = int64(0)
stats["freelist_count"] = int64(0)
stats["total_size_bytes"] = totalSize
stats["free_size_bytes"] = int64(0)
stats["used_size_bytes"] = totalSize
}
return stats, nil
}
// SQLite:获取数据库页面统计
var pageCount, pageSize, freelistCount int64
err = cacheDB.QueryRow("PRAGMA page_count").Scan(&pageCount)
if err == nil {
@@ -458,7 +599,7 @@ func SaveStatistics(stats map[string]int64) error {
cacheMutex.Lock()
defer cacheMutex.Unlock()
_, err := cacheDB.Exec(`
_, err := cacheDB.Exec(q(`
UPDATE statistics
SET total_requests = ?,
search_requests = ?,
@@ -469,10 +610,11 @@ func SaveStatistics(stats map[string]int64) error {
audio_requests = ?,
video_requests = ?,
category_requests = ?,
updated_at = strftime('%s', 'now')
updated_at = ?
WHERE id = 1
`, stats["total"], stats["search"], stats["detail"], stats["catalog"],
stats["content"], stats["manga"], stats["audio"], stats["video"], stats["category"])
`), stats["total"], stats["search"], stats["detail"], stats["catalog"],
stats["content"], stats["manga"], stats["audio"], stats["video"], stats["category"],
time.Now().Unix())
if err != nil {
cacheLogger.Error("保存统计数据失败: " + err.Error())
+2 -2
View File
@@ -9,8 +9,8 @@ import (
// getContentConfig 获取内容配置(带默认值)
func getContentConfig() config.ContentConfig {
if config.AppConfig != nil {
return config.AppConfig.Fanqie.Content
if config.GetAppConfig() != nil {
return config.GetAppConfig().Fanqie.Content
}
// 返回默认配置
return config.ContentConfig{
+15 -44
View File
@@ -385,6 +385,14 @@ func (p *DeviceKeyPool) loadFromDB() error {
return nil
}
// boolToInt 布尔转整数存储(is_valid 列为 INTEGER,PostgreSQL 严格要求类型匹配)
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
// saveDeviceToDB 保存设备到数据库
func (p *DeviceKeyPool) saveDeviceToDB(item *DeviceKeyItem) error {
if cacheDB == nil {
@@ -394,23 +402,15 @@ func (p *DeviceKeyPool) saveDeviceToDB(item *DeviceKeyItem) error {
cacheMutex.Lock()
defer cacheMutex.Unlock()
result, err := cacheDB.Exec(`
// RETURNING id 同时兼容 SQLite(3.35+) 与 PostgreSQL,替代 LastInsertId()
err := cacheDB.QueryRow(q(`
INSERT INTO device_pool (device_header, key, key_ver, is_valid, created_at, last_used_at)
VALUES (?, ?, ?, ?, ?, ?)
`, item.DeviceHeader, item.Key, item.KeyVer, item.IsValid,
item.CreatedAt.Unix(), item.LastUsedAt.Unix())
RETURNING id
`), item.DeviceHeader, item.Key, item.KeyVer, boolToInt(item.IsValid),
item.CreatedAt.Unix(), item.LastUsedAt.Unix()).Scan(&item.ID)
if err != nil {
return err
}
// 获取插入的ID
id, err := result.LastInsertId()
if err == nil {
item.ID = int(id)
}
return nil
}
// updateDeviceInDB 更新设备信息
@@ -422,11 +422,11 @@ func (p *DeviceKeyPool) updateDeviceInDB(item *DeviceKeyItem) error {
cacheMutex.Lock()
defer cacheMutex.Unlock()
_, err := cacheDB.Exec(`
_, err := cacheDB.Exec(q(`
UPDATE device_pool
SET device_header = ?, key = ?, key_ver = ?, is_valid = ?, last_used_at = ?
WHERE id = ?
`, item.DeviceHeader, item.Key, item.KeyVer, item.IsValid,
`), item.DeviceHeader, item.Key, item.KeyVer, boolToInt(item.IsValid),
item.LastUsedAt.Unix(), item.ID)
return err
@@ -454,35 +454,6 @@ func (p *DeviceKeyPool) removeInvalidFromDB() error {
return nil
}
// InitDevicePoolTable 初始化设备池表
func InitDevicePoolTable() error {
if cacheDB == nil {
return fmt.Errorf("数据库未初始化")
}
_, err := cacheDB.Exec(`CREATE TABLE IF NOT EXISTS device_pool (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_header TEXT NOT NULL,
key TEXT NOT NULL,
key_ver INTEGER NOT NULL,
is_valid INTEGER DEFAULT 1,
created_at INTEGER NOT NULL,
last_used_at INTEGER NOT NULL
)`)
if err != nil {
return err
}
// 创建索引
_, err = cacheDB.Exec(`CREATE INDEX IF NOT EXISTS idx_device_pool_valid ON device_pool(is_valid)`)
if err != nil {
poolLogger.Warning("创建索引失败: " + err.Error())
}
return nil
}
// GetDeviceHeaderFromPool 从池中获取设备header(兼容旧接口)
func GetDeviceHeaderFromPool() string {
pool := GetDeviceKeyPool()
+20
View File
@@ -0,0 +1,20 @@
package fqapi
import "testing"
// TestRebind 验证占位符方言转换
func TestRebind(t *testing.T) {
in := "INSERT INTO cache (key, value) VALUES (?, ?)"
if got := q(in); got != in {
t.Errorf("sqlite 下应保持 ?: %s", got)
}
dbDriver = "pgx"
defer func() { dbDriver = "sqlite3" }()
want := "INSERT INTO cache (key, value) VALUES ($1, $2)"
if got := q(in); got != want {
t.Errorf("pg 下应转换占位符: got %s want %s", got, want)
}
}
+85
View File
@@ -0,0 +1,85 @@
package fqapi
import (
"context"
"encoding/json"
"fmt"
"time"
)
// audioProxyEntry 音频代理缓存条目(Redis 版)
type audioProxyEntry struct {
OriginalURL string `json:"original_url"`
DeviceID string `json:"device_id"`
Headers map[string]string `json:"headers"`
}
const (
redisCacheKeyPrefix = "fq:cache:"
redisAudioKeyPrefix = "fq:audio:"
// audioCacheTTL 音频代理缓存有效期(匹配 CDN 链接有效期)
audioCacheTTL = 2 * time.Hour
)
// redisGetCache 从 Redis 获取正文缓存
func redisGetCache(key string) string {
val, err := redisCache.Get(context.Background(), redisCacheKeyPrefix+key).Result()
if err != nil {
return "" // miss 或错误都视为未命中
}
var cacheData CacheData
if err := json.Unmarshal([]byte(val), &cacheData); err != nil {
cacheLogger.Error("解析Redis缓存数据失败: " + err.Error())
return ""
}
cacheLogger.Debug("Redis缓存命中: " + key)
if time.Now().Unix() < int64(cacheData.Expi) {
return cacheData.Data
}
return ""
}
// redisUpdateCache 写入 Redis 正文缓存(TTL 与过期时间一致)
func redisUpdateCache(key string, value string, expi int) error {
cacheData := CacheData{
Data: value,
Expi: float64(time.Now().Unix() + int64(expi)),
}
jsonData, err := json.Marshal(cacheData)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return redisCache.Set(ctx, redisCacheKeyPrefix+key, jsonData, time.Duration(expi)*time.Second).Err()
}
// saveAudioProxyEntry 将音频代理信息存入 Redis
func saveAudioProxyEntry(key string, entry *audioProxyEntry) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
jsonData, _ := json.Marshal(entry)
if err := redisCache.Set(ctx, redisAudioKeyPrefix+key, jsonData, audioCacheTTL).Err(); err != nil {
audioLogger.Error("保存音频缓存到Redis失败: " + err.Error())
}
}
// loadAudioProxyEntry 从 Redis 读取音频代理信息,未命中或过期返回错误
func loadAudioProxyEntry(key string) (*audioProxyEntry, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
val, err := redisCache.Get(ctx, redisAudioKeyPrefix+key).Result()
if err != nil {
return nil, fmt.Errorf("音频信息不存在或已过期,请重新调用 /audio 接口")
}
var entry audioProxyEntry
if err := json.Unmarshal([]byte(val), &entry); err != nil {
return nil, fmt.Errorf("解析音频缓存失败: %w", err)
}
return &entry, nil
}
+3 -6
View File
@@ -9,8 +9,7 @@ import (
// HandleSearch 搜索处理
func HandleSearch(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementSearch()
incrementStat("total", "search")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
query := r.URL.Query().Get("query")
@@ -41,8 +40,7 @@ func HandleSearch(w http.ResponseWriter, r *http.Request) {
// HandleDetail 详情处理
func HandleDetail(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementDetail()
incrementStat("total", "detail")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
bookID := r.URL.Query().Get("book_id")
@@ -58,8 +56,7 @@ func HandleDetail(w http.ResponseWriter, r *http.Request) {
// HandleCatalog 目录处理
func HandleCatalog(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementCatalog()
incrementStat("total", "catalog")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
bookID := r.URL.Query().Get("book_id")
+2 -4
View File
@@ -8,8 +8,7 @@ import (
// HandleCategoryFront 分类列表处理
func HandleCategoryFront(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementCategory()
incrementStat("total", "category")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
tab := r.URL.Query().Get("tab")
@@ -25,8 +24,7 @@ func HandleCategoryFront(w http.ResponseWriter, r *http.Request) {
// HandleCategoryLanding 分类详情处理
func HandleCategoryLanding(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementCategory()
incrementStat("total", "category")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
categoryID := r.URL.Query().Get("category_id")
+33 -50
View File
@@ -15,40 +15,39 @@ var (
deviceHeader string
// 请求统计
totalRequests int64
searchRequests int64
detailRequests int64
catalogRequests int64
contentRequests int64
mangaRequests int64
audioRequests int64
videoRequests int64
categoryRequests int64
requestStats = newRequestStats()
// 统计保存定时器
statsSaveTicker *time.Ticker
)
// 统计函数
func incrementTotal() { atomic.AddInt64(&totalRequests, 1) }
func incrementSearch() { atomic.AddInt64(&searchRequests, 1) }
func incrementDetail() { atomic.AddInt64(&detailRequests, 1) }
func incrementCatalog() { atomic.AddInt64(&catalogRequests, 1) }
func incrementContent() { atomic.AddInt64(&contentRequests, 1) }
func incrementManga() { atomic.AddInt64(&mangaRequests, 1) }
func incrementAudio() { atomic.AddInt64(&audioRequests, 1) }
func incrementVideo() { atomic.AddInt64(&videoRequests, 1) }
func incrementCategory() { atomic.AddInt64(&categoryRequests, 1) }
// statsKinds 统计类别
var statsKinds = []string{"total", "search", "detail", "catalog", "content", "manga", "audio", "video", "category"}
func getTotal() int64 { return atomic.LoadInt64(&totalRequests) }
func getSearch() int64 { return atomic.LoadInt64(&searchRequests) }
func getDetail() int64 { return atomic.LoadInt64(&detailRequests) }
func getCatalog() int64 { return atomic.LoadInt64(&catalogRequests) }
func getContent() int64 { return atomic.LoadInt64(&contentRequests) }
func getManga() int64 { return atomic.LoadInt64(&mangaRequests) }
func getAudio() int64 { return atomic.LoadInt64(&audioRequests) }
func getVideo() int64 { return atomic.LoadInt64(&videoRequests) }
func getCategory() int64 { return atomic.LoadInt64(&categoryRequests) }
// newRequestStats 初始化各类计数器
func newRequestStats() map[string]*atomic.Int64 {
m := make(map[string]*atomic.Int64, len(statsKinds))
for _, k := range statsKinds {
m[k] = &atomic.Int64{}
}
return m
}
// incrementStat 原子递增请求计数
func incrementStat(kinds ...string) {
for _, k := range kinds {
requestStats[k].Add(1)
}
}
// getStatsSnapshot 获取所有计数器的快照
func getStatsSnapshot() map[string]int64 {
snapshot := make(map[string]int64, len(requestStats))
for k, c := range requestStats {
snapshot[k] = c.Load()
}
return snapshot
}
// SetDeviceHeader 设置设备header
func SetDeviceHeader(header string) {
@@ -63,32 +62,16 @@ func LoadStatistics() {
return
}
atomic.StoreInt64(&totalRequests, stats["total"])
atomic.StoreInt64(&searchRequests, stats["search"])
atomic.StoreInt64(&detailRequests, stats["detail"])
atomic.StoreInt64(&catalogRequests, stats["catalog"])
atomic.StoreInt64(&contentRequests, stats["content"])
atomic.StoreInt64(&mangaRequests, stats["manga"])
atomic.StoreInt64(&audioRequests, stats["audio"])
atomic.StoreInt64(&videoRequests, stats["video"])
atomic.StoreInt64(&categoryRequests, stats["category"])
for k, v := range stats {
if c, ok := requestStats[k]; ok {
c.Store(v)
}
}
}
// SaveStatistics 保存统计数据到数据库
func SaveStatistics() {
stats := map[string]int64{
"total": atomic.LoadInt64(&totalRequests),
"search": atomic.LoadInt64(&searchRequests),
"detail": atomic.LoadInt64(&detailRequests),
"catalog": atomic.LoadInt64(&catalogRequests),
"content": atomic.LoadInt64(&contentRequests),
"manga": atomic.LoadInt64(&mangaRequests),
"audio": atomic.LoadInt64(&audioRequests),
"video": atomic.LoadInt64(&videoRequests),
"category": atomic.LoadInt64(&categoryRequests),
}
if err := fqapi.SaveStatistics(stats); err != nil {
if err := fqapi.SaveStatistics(getStatsSnapshot()); err != nil {
fmt.Printf("保存统计数据失败: %v\n", err)
}
}
+1 -2
View File
@@ -9,8 +9,7 @@ import (
// HandleContent 内容处理
func HandleContent(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementContent()
incrementStat("total", "content")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// 从查询参数获取item_id
+4 -7
View File
@@ -10,8 +10,7 @@ import (
// HandleAudio 音频处理
func HandleAudio(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementAudio()
incrementStat("total", "audio")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
itemIDs := r.URL.Query().Get("item_ids")
@@ -29,7 +28,7 @@ func HandleAudio(w http.ResponseWriter, r *http.Request) {
// HandleAudioProxy 音频代理处理
func HandleAudioProxy(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementStat("total")
// 从URL路径提取key: /audio/proxy/{key}
path := r.URL.Path
@@ -60,8 +59,7 @@ func HandleAudioProxy(w http.ResponseWriter, r *http.Request) {
// HandleVideo 视频处理
func HandleVideo(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementVideo()
incrementStat("total", "video")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
itemIDs := r.URL.Query().Get("item_ids")
@@ -77,8 +75,7 @@ func HandleVideo(w http.ResponseWriter, r *http.Request) {
// HandleManga 漫画处理
func HandleManga(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementManga()
incrementStat("total", "manga")
itemID := r.URL.Query().Get("item_ids")
if itemID == "" {
itemID = r.URL.Query().Get("item_id")
+3 -3
View File
@@ -70,7 +70,7 @@ func handleReviewSuccess(w http.ResponseWriter, data interface{}) {
// HandleChapterReview 处理章节评论请求(混合数据)
func HandleChapterReview(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementStat("total")
// 验证必需参数
params, err := validateParams(r, "item_id")
@@ -94,7 +94,7 @@ func HandleChapterReview(w http.ResponseWriter, r *http.Request) {
// HandleReview 处理段落评论请求
func HandleReview(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementStat("total")
// 验证必需参数
params, err := validateParams(r, "item_id")
@@ -141,7 +141,7 @@ func HandleReview(w http.ResponseWriter, r *http.Request) {
// HandleReviewHTML 处理段落评论 HTML 显示
func HandleReviewHTML(w http.ResponseWriter, r *http.Request) {
incrementTotal()
incrementStat("total")
// 验证必需参数
params, err := validateParams(r, "item_id", "para")
+10 -18
View File
@@ -72,15 +72,7 @@ func HandleStatus(w http.ResponseWriter, r *http.Request) {
}
// 获取统计数据
total := getTotal()
search := getSearch()
detail := getDetail()
catalog := getCatalog()
content := getContent()
manga := getManga()
audio := getAudio()
video := getVideo()
category := getCategory()
stats := getStatsSnapshot()
// 获取设备密钥池状态
poolStatus := getDeviceKeyPoolStatus()
@@ -857,15 +849,15 @@ func HandleStatus(w http.ResponseWriter, r *http.Request) {
poolStatus["valid"],
poolStatus["invalid"],
poolStatus["current_index"],
total,
search,
detail,
catalog,
content,
manga,
audio,
video,
category,
stats["total"],
stats["search"],
stats["detail"],
stats["catalog"],
stats["content"],
stats["manga"],
stats["audio"],
stats["video"],
stats["category"],
getDBCacheCount(),
getCacheStatus(),
getCleanupStatus(),