mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-04 23:42:13 +08:00
Compare commits
332 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bae525026 | ||
|
|
df00805a2a | ||
|
|
a88ee96518 | ||
|
|
3cc2f9bd57 | ||
|
|
d1b684b782 | ||
|
|
6460d4ad3a | ||
|
|
19ea392d5d | ||
|
|
fb4d016176 | ||
|
|
afec747d9e | ||
|
|
7388fcce41 | ||
|
|
a6f9f9f968 | ||
|
|
29759721e0 | ||
|
|
1941b20521 | ||
|
|
e6969acb50 | ||
|
|
9489531431 | ||
|
|
32b7c0ca9b | ||
|
|
4ac57b4edf | ||
|
|
685a1e0ba3 | ||
|
|
e350aab1bd | ||
|
|
0dd6986e28 | ||
|
|
6d0102a70c | ||
|
|
f96a2a18c1 | ||
|
|
f955b04a6f | ||
|
|
2fd6ac319b | ||
|
|
82fbf452a8 | ||
|
|
ba69736f55 | ||
|
|
c75c6b6858 | ||
|
|
de61745bb2 | ||
|
|
3fab0fcd4c | ||
|
|
03bcd94ae5 | ||
|
|
0343bc7777 | ||
|
|
565d19acfd | ||
|
|
960acf1982 | ||
|
|
ece911521e | ||
|
|
5d95e59742 | ||
|
|
01d084bbfd | ||
|
|
7918fc2844 | ||
|
|
31b30a6df2 | ||
|
|
d217b59e0b | ||
|
|
169a4b9d32 | ||
|
|
15f3ffb165 | ||
|
|
02db1010dd | ||
|
|
935ea66681 | ||
|
|
26060e702f | ||
|
|
65d4ca2563 | ||
|
|
3c619a8da5 | ||
|
|
ded9b6c14e | ||
|
|
609abbbd7c | ||
|
|
1b4e504fad | ||
|
|
0a3a445828 | ||
|
|
c7e18bd5be | ||
|
|
083d202fe4 | ||
|
|
8365a8328b | ||
|
|
58f21e4b3a | ||
|
|
5bd7408b2f | ||
|
|
c671e8dd1d | ||
|
|
a3aed3c4c3 | ||
|
|
c008649584 | ||
|
|
516f8f287c | ||
|
|
66148690c6 | ||
|
|
cadd7f546f | ||
|
|
a3ff317f1c | ||
|
|
d8d4b0c0c7 | ||
|
|
d616f8c854 | ||
|
|
b6fa8b8eec | ||
|
|
36d2e6999b | ||
|
|
076c00063d | ||
|
|
ea8104c6a2 | ||
|
|
ca3e9336e1 | ||
|
|
f92ab48166 | ||
|
|
c10267ce2b | ||
|
|
9bd6a62ab3 | ||
|
|
0dbea6ca58 | ||
|
|
6523b23221 | ||
|
|
29c406dda0 | ||
|
|
483c8f246d | ||
|
|
645f283108 | ||
|
|
da6fd45000 | ||
|
|
fb3ef5f388 | ||
|
|
86bc76e352 | ||
|
|
644058174e | ||
|
|
4573868c08 | ||
|
|
09166a52f8 | ||
|
|
aaac1aaca9 | ||
|
|
59898c16c6 | ||
|
|
0dacdf480b | ||
|
|
fdf9f68298 | ||
|
|
7be5e1734c | ||
|
|
bfe414670f | ||
|
|
e435a46db5 | ||
|
|
84bd881e68 | ||
|
|
a901117b8c | ||
|
|
6bccb8a8a6 | ||
|
|
3de1e0e485 | ||
|
|
492b852a1f | ||
|
|
8a137405d4 | ||
|
|
f431f5ed72 | ||
|
|
980fc9608f | ||
|
|
07be258dca | ||
|
|
dbdb29594c | ||
|
|
53d55bb92f | ||
|
|
3f3efff065 | ||
|
|
57b078f2c7 | ||
|
|
1fc6ef3d4f | ||
|
|
c2567831d9 | ||
|
|
e8671fd7c2 | ||
|
|
4950ee48a0 | ||
|
|
5fa45f3b8c | ||
|
|
3b6584cc8d | ||
|
|
7be1195281 | ||
|
|
1fae8d086d | ||
|
|
10636d8a1f | ||
|
|
c67f02eaf0 | ||
|
|
0b32f61062 | ||
|
|
2ee6c26676 | ||
|
|
a89477ddf5 | ||
|
|
2f520c8d47 | ||
|
|
33db7a0fb6 | ||
|
|
50b9897182 | ||
|
|
f8ac5538e2 | ||
|
|
1985be26b2 | ||
|
|
fdfc739b72 | ||
|
|
bde9dbc57a | ||
|
|
80510e5f16 | ||
|
|
773f20ed5e | ||
|
|
f323174d07 | ||
|
|
987589eabc | ||
|
|
1004bd86ac | ||
|
|
03f69dd394 | ||
|
|
d14c24bbf3 | ||
|
|
48dc011b2a | ||
|
|
b341810e60 | ||
|
|
46d9aee6dd | ||
|
|
36a1a7998b | ||
|
|
40498aac9d | ||
|
|
440b87094a | ||
|
|
0832dfb32e | ||
|
|
be09188bda | ||
|
|
5d2219d299 | ||
|
|
900cce20a1 | ||
|
|
36bb327024 | ||
|
|
5d9667d27a | ||
|
|
fad04ca995 | ||
|
|
074bd0dfda | ||
|
|
b41fa5e15f | ||
|
|
beceb45d23 | ||
|
|
9450edf462 | ||
|
|
785a7397f8 | ||
|
|
3d1f03c286 | ||
|
|
8ff40f52e0 | ||
|
|
6577f2ef03 | ||
|
|
41d0383fb7 | ||
|
|
1cf51b14f7 | ||
|
|
372e04f69a | ||
|
|
e2107ce45e | ||
|
|
a817cafe3d | ||
|
|
ab14df043a | ||
|
|
5feff6b1e5 | ||
|
|
06b0f62e79 | ||
|
|
40d110efe4 | ||
|
|
f23318fbcf | ||
|
|
cbab49d65f | ||
|
|
b5a3b3db66 | ||
|
|
9cafa46dd3 | ||
|
|
f6bff97d26 | ||
|
|
d04b47b3ca | ||
|
|
862199143e | ||
|
|
57e8abcb63 | ||
|
|
ed31c54961 | ||
|
|
4bfa69bffa | ||
|
|
2857fa2ef7 | ||
|
|
e681431454 | ||
|
|
5b568aa9d4 | ||
|
|
471943269c | ||
|
|
28a5e2f0e6 | ||
|
|
b4c22ce6ce | ||
|
|
5248097f90 | ||
|
|
8e2c22d0bd | ||
|
|
888f2936ad | ||
|
|
4e894bac1f | ||
|
|
f96acf6e27 | ||
|
|
be56a282f2 | ||
|
|
2459eafb71 | ||
|
|
ed681d0830 | ||
|
|
5f4eb9f9d0 | ||
|
|
d1cd5c0a73 | ||
|
|
5429c74c10 | ||
|
|
3734abed4c | ||
|
|
abf5de69fb | ||
|
|
7582dc53d2 | ||
|
|
174d7c774d | ||
|
|
a9518cc5be | ||
|
|
2f190d812a | ||
|
|
d411cf4472 | ||
|
|
1ae49b9ead | ||
|
|
0bf162f64a | ||
|
|
6423636177 | ||
|
|
b6aaee01ce | ||
|
|
3511376c2c | ||
|
|
584cfc3db2 | ||
|
|
eaa7d899f0 | ||
|
|
84cc651b46 | ||
|
|
b7243660c4 | ||
|
|
e722992439 | ||
|
|
fff1d54858 | ||
|
|
a5f29019d9 | ||
|
|
208c5380f4 | ||
|
|
29191af877 | ||
|
|
2d6066f985 | ||
|
|
3ea5e5c33a | ||
|
|
dbd7969a3e | ||
|
|
af3069073a | ||
|
|
65661f24e2 | ||
|
|
ed2eba9028 | ||
|
|
10c1590b1d | ||
|
|
114e172603 | ||
|
|
09c8380b3d | ||
|
|
ba567babf4 | ||
|
|
9403aa9bd1 | ||
|
|
34b8bbcbe4 | ||
|
|
6b36992d34 | ||
|
|
6533a4647d | ||
|
|
9c910c2049 | ||
|
|
43dc23a47d | ||
|
|
61a2bf469a | ||
|
|
fe1d46a8ea | ||
|
|
a88bb8684f | ||
|
|
c7b42148a5 | ||
|
|
bc1abb6a23 | ||
|
|
d307d48def | ||
|
|
1bb40084fc | ||
|
|
8f0efa16ca | ||
|
|
8da5fac69e | ||
|
|
e2cdb6c758 | ||
|
|
ef2c35dbb1 | ||
|
|
04a1a7c2b5 | ||
|
|
d21d70a5cf | ||
|
|
e73b778d2b | ||
|
|
723102766b | ||
|
|
a4a46a8618 | ||
|
|
6ae82e04d5 | ||
|
|
19cca11e00 | ||
|
|
c8f87a9c92 | ||
|
|
f1e884ce2b | ||
|
|
86f3124720 | ||
|
|
ae6fed15cc | ||
|
|
4b309fa8b5 | ||
|
|
378e476e48 | ||
|
|
2a1067c82b | ||
|
|
a54b81cf74 | ||
|
|
2d4236f76e | ||
|
|
166080b29c | ||
|
|
3b0910f664 | ||
|
|
e489996713 | ||
|
|
54fe363257 | ||
|
|
84ced1c497 | ||
|
|
b161312183 | ||
|
|
1dd3158c7e | ||
|
|
1f647b120a | ||
|
|
7d0a30fa8f | ||
|
|
d95e04fd1f | ||
|
|
5d1c51a37f | ||
|
|
58912d4ac5 | ||
|
|
6114f69cca | ||
|
|
d6c2921f2b | ||
|
|
29ca1290b3 | ||
|
|
3fcb0cc37c | ||
|
|
61c73287dc | ||
|
|
89905ec43d | ||
|
|
2bfb16291f | ||
|
|
d367d1cde6 | ||
|
|
3c46f7d266 | ||
|
|
16131c3d3f | ||
|
|
d7011163b8 | ||
|
|
fc8a39e0f5 | ||
|
|
9da80e9fda | ||
|
|
bb5a5dd65e | ||
|
|
53e1c8b268 | ||
|
|
d876686a00 | ||
|
|
7546a56736 | ||
|
|
00caf0bcd8 | ||
|
|
9634494ba9 | ||
|
|
e1ac0db05c | ||
|
|
6f3e77a2df | ||
|
|
4a20a2a8ba | ||
|
|
bc3ca5f068 | ||
|
|
fd43be8d0b | ||
|
|
836ba14b70 | ||
|
|
a14dfb769a | ||
|
|
2588fa6a8f | ||
|
|
f6ca701917 | ||
|
|
a84604dceb | ||
|
|
e75d3e3584 | ||
|
|
8226a4ce4d | ||
|
|
65c0d8b51f | ||
|
|
a9e256ce8c | ||
|
|
7e1674e43a | ||
|
|
fc104dfb56 | ||
|
|
0e514ed80b | ||
|
|
782a54a8a1 | ||
|
|
4e01126ff2 | ||
|
|
55b56328da | ||
|
|
ce764bf2d9 | ||
|
|
d71537d431 | ||
|
|
ae1ba45350 | ||
|
|
c4182f8c33 | ||
|
|
028f8aaa97 | ||
|
|
d3f11fdbd3 | ||
|
|
8672b2f3ec | ||
|
|
de753a149e | ||
|
|
2d4bbbf49d | ||
|
|
792bef615c | ||
|
|
000a943cce | ||
|
|
f82e346f02 | ||
|
|
d8e405511e | ||
|
|
74d35f0860 | ||
|
|
de7ff902de | ||
|
|
317f26f0bf | ||
|
|
dd96ada3c6 | ||
|
|
9b120e68b8 | ||
|
|
377bffe281 | ||
|
|
31fe017888 | ||
|
|
99250ec527 | ||
|
|
dcf5f60237 | ||
|
|
399dd78b2a | ||
|
|
78d0ca3775 | ||
|
|
618a614cbf | ||
|
|
99dc3b59bc | ||
|
|
d9e345f23d | ||
|
|
a505d992ee | ||
|
|
13262a5698 | ||
|
|
bece1b5201 |
2
.github/workflows/backend-ci.yml
vendored
2
.github/workflows/backend-ci.yml
vendored
@@ -44,4 +44,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: v2.7
|
version: v2.7
|
||||||
args: --timeout=5m
|
args: --timeout=5m
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
2
.github/workflows/security-scan.yml
vendored
2
.github/workflows/security-scan.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
working-directory: backend
|
working-directory: backend
|
||||||
run: |
|
run: |
|
||||||
go install github.com/securego/gosec/v2/cmd/gosec@latest
|
go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||||
gosec -severity high -confidence high ./...
|
gosec -conf .gosec.json -severity high -confidence high ./...
|
||||||
|
|
||||||
frontend-security:
|
frontend-security:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -121,7 +121,6 @@ AGENTS.md
|
|||||||
scripts
|
scripts
|
||||||
.code-review-state
|
.code-review-state
|
||||||
openspec/
|
openspec/
|
||||||
docs/
|
|
||||||
code-reviews/
|
code-reviews/
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
backend/cmd/server/server
|
backend/cmd/server/server
|
||||||
@@ -129,4 +128,8 @@ deploy/docker-compose.override.yml
|
|||||||
.gocache/
|
.gocache/
|
||||||
vite.config.js
|
vite.config.js
|
||||||
docs/*
|
docs/*
|
||||||
.serena/
|
.serena/
|
||||||
|
.codex/
|
||||||
|
frontend/coverage/
|
||||||
|
aicodex
|
||||||
|
|
||||||
|
|||||||
25
DEV_GUIDE.md
25
DEV_GUIDE.md
@@ -209,7 +209,30 @@ git add ent/ # 生成的文件也要提交
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 坑 10:PR 提交前检查清单
|
### 坑 10:前端测试看似正常,但后端调用失败(模型映射被批量误改)
|
||||||
|
|
||||||
|
**典型现象**:
|
||||||
|
- 前端按钮点测看起来正常;
|
||||||
|
- 实际通过 API/客户端调用时返回 `Service temporarily unavailable` 或提示无可用账号;
|
||||||
|
- 常见于 OpenAI 账号(例如 Codex 模型)在批量修改后突然不可用。
|
||||||
|
|
||||||
|
**根因**:
|
||||||
|
- OpenAI 账号编辑页默认不显式展示映射规则,容易让人误以为“没映射也没关系”;
|
||||||
|
- 但在**批量修改同时选中不同平台账号**(OpenAI + Antigravity/Gemini)时,模型白名单/映射可能被跨平台策略覆盖;
|
||||||
|
- 结果是 OpenAI 账号的关键模型映射丢失或被改坏,后端选不到可用账号。
|
||||||
|
|
||||||
|
**修复方案(按优先级)**:
|
||||||
|
1. **快速修复(推荐)**:在批量修改中补回正确的透传映射(例如 `gpt-5.3-codex -> gpt-5.3-codex-spark`)。
|
||||||
|
2. **彻底重建**:删除并重新添加全部相关账号(最稳但成本高)。
|
||||||
|
|
||||||
|
**关键经验**:
|
||||||
|
- 如果某模型已被软件内置默认映射覆盖,通常不需要额外再加透传;
|
||||||
|
- 但当上游模型更新快于本仓库默认映射时,**手动批量添加透传映射**是最简单、最低风险的临时兜底方案;
|
||||||
|
- 批量操作前尽量按平台分组,不要混选不同平台账号。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 坑 11:PR 提交前检查清单
|
||||||
|
|
||||||
提交 PR 前务必本地验证:
|
提交 PR 前务必本地验证:
|
||||||
|
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -36,7 +36,7 @@ RUN pnpm run build
|
|||||||
FROM ${GOLANG_IMAGE} AS backend-builder
|
FROM ${GOLANG_IMAGE} AS backend-builder
|
||||||
|
|
||||||
# Build arguments for version info (set by CI)
|
# Build arguments for version info (set by CI)
|
||||||
ARG VERSION=docker
|
ARG VERSION=
|
||||||
ARG COMMIT=docker
|
ARG COMMIT=docker
|
||||||
ARG DATE
|
ARG DATE
|
||||||
ARG GOPROXY
|
ARG GOPROXY
|
||||||
@@ -61,9 +61,13 @@ COPY backend/ ./
|
|||||||
COPY --from=frontend-builder /app/backend/internal/web/dist ./internal/web/dist
|
COPY --from=frontend-builder /app/backend/internal/web/dist ./internal/web/dist
|
||||||
|
|
||||||
# Build the binary (BuildType=release for CI builds, embed frontend)
|
# Build the binary (BuildType=release for CI builds, embed frontend)
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
# Version precedence: build arg VERSION > cmd/server/VERSION
|
||||||
|
RUN VERSION_VALUE="${VERSION}" && \
|
||||||
|
if [ -z "${VERSION_VALUE}" ]; then VERSION_VALUE="$(tr -d '\r\n' < ./cmd/server/VERSION)"; fi && \
|
||||||
|
DATE_VALUE="${DATE:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}" && \
|
||||||
|
CGO_ENABLED=0 GOOS=linux go build \
|
||||||
-tags embed \
|
-tags embed \
|
||||||
-ldflags="-s -w -X main.Commit=${COMMIT} -X main.Date=${DATE:-$(date -u +%Y-%m-%dT%H:%M:%SZ)} -X main.BuildType=release" \
|
-ldflags="-s -w -X main.Version=${VERSION_VALUE} -X main.Commit=${COMMIT} -X main.Date=${DATE_VALUE} -X main.BuildType=release" \
|
||||||
-o /app/sub2api \
|
-o /app/sub2api \
|
||||||
./cmd/server
|
./cmd/server
|
||||||
|
|
||||||
|
|||||||
5
Makefile
5
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: build build-backend build-frontend test test-backend test-frontend
|
.PHONY: build build-backend build-frontend test test-backend test-frontend secret-scan
|
||||||
|
|
||||||
# 一键编译前后端
|
# 一键编译前后端
|
||||||
build: build-backend build-frontend
|
build: build-backend build-frontend
|
||||||
@@ -20,3 +20,6 @@ test-backend:
|
|||||||
test-frontend:
|
test-frontend:
|
||||||
@pnpm --dir frontend run lint:check
|
@pnpm --dir frontend run lint:check
|
||||||
@pnpm --dir frontend run typecheck
|
@pnpm --dir frontend run typecheck
|
||||||
|
|
||||||
|
secret-scan:
|
||||||
|
@python3 tools/secret_scan.py
|
||||||
|
|||||||
@@ -363,6 +363,12 @@ default:
|
|||||||
rate_multiplier: 1.0
|
rate_multiplier: 1.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sora Status (Temporarily Unavailable)
|
||||||
|
|
||||||
|
> ⚠️ Sora-related features are temporarily unavailable due to technical issues in upstream integration and media delivery.
|
||||||
|
> Please do not rely on Sora in production at this time.
|
||||||
|
> Existing `gateway.sora_*` configuration keys are reserved and may not take effect until these issues are resolved.
|
||||||
|
|
||||||
Additional security-related options are available in `config.yaml`:
|
Additional security-related options are available in `config.yaml`:
|
||||||
|
|
||||||
- `cors.allowed_origins` for CORS allowlist
|
- `cors.allowed_origins` for CORS allowlist
|
||||||
|
|||||||
60
README_CN.md
60
README_CN.md
@@ -139,6 +139,8 @@ curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install
|
|||||||
|
|
||||||
使用 Docker Compose 部署,包含 PostgreSQL 和 Redis 容器。
|
使用 Docker Compose 部署,包含 PostgreSQL 和 Redis 容器。
|
||||||
|
|
||||||
|
如果你的服务器是 **Ubuntu 24.04**,建议直接参考:`deploy/ubuntu24-docker-compose-aicodex.md`,其中包含「安装最新版 Docker + docker-compose-aicodex.yml 部署」的完整步骤。
|
||||||
|
|
||||||
#### 前置条件
|
#### 前置条件
|
||||||
|
|
||||||
- Docker 20.10+
|
- Docker 20.10+
|
||||||
@@ -370,6 +372,33 @@ default:
|
|||||||
rate_multiplier: 1.0
|
rate_multiplier: 1.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sora 功能状态(暂不可用)
|
||||||
|
|
||||||
|
> ⚠️ 当前 Sora 相关功能因上游接入与媒体链路存在技术问题,暂时不可用。
|
||||||
|
> 现阶段请勿在生产环境依赖 Sora 能力。
|
||||||
|
> 文档中的 `gateway.sora_*` 配置仅作预留,待技术问题修复后再恢复可用。
|
||||||
|
|
||||||
|
### Sora 媒体签名 URL(功能恢复后可选)
|
||||||
|
|
||||||
|
当配置 `gateway.sora_media_signing_key` 且 `gateway.sora_media_signed_url_ttl_seconds > 0` 时,网关会将 Sora 输出的媒体地址改写为临时签名 URL(`/sora/media-signed/...`)。这样无需 API Key 即可在浏览器中直接访问,且具备过期控制与防篡改能力(签名包含 path + query)。
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
gateway:
|
||||||
|
# /sora/media 是否强制要求 API Key(默认 false)
|
||||||
|
sora_media_require_api_key: false
|
||||||
|
# 媒体临时签名密钥(为空则禁用签名)
|
||||||
|
sora_media_signing_key: "your-signing-key"
|
||||||
|
# 临时签名 URL 有效期(秒)
|
||||||
|
sora_media_signed_url_ttl_seconds: 900
|
||||||
|
```
|
||||||
|
|
||||||
|
> 若未配置签名密钥,`/sora/media-signed` 将返回 503。
|
||||||
|
> 如需更严格的访问控制,可将 `sora_media_require_api_key` 设为 true,仅允许携带 API Key 的 `/sora/media` 访问。
|
||||||
|
|
||||||
|
访问策略说明:
|
||||||
|
- `/sora/media`:内部调用或客户端携带 API Key 才能下载
|
||||||
|
- `/sora/media-signed`:外部可访问,但有签名 + 过期控制
|
||||||
|
|
||||||
`config.yaml` 还支持以下安全相关配置:
|
`config.yaml` 还支持以下安全相关配置:
|
||||||
|
|
||||||
- `cors.allowed_origins` 配置 CORS 白名单
|
- `cors.allowed_origins` 配置 CORS 白名单
|
||||||
@@ -383,6 +412,14 @@ default:
|
|||||||
- `server.trusted_proxies` 启用可信代理解析 X-Forwarded-For
|
- `server.trusted_proxies` 启用可信代理解析 X-Forwarded-For
|
||||||
- `turnstile.required` 在 release 模式强制启用 Turnstile
|
- `turnstile.required` 在 release 模式强制启用 Turnstile
|
||||||
|
|
||||||
|
**网关防御纵深建议(重点)**
|
||||||
|
|
||||||
|
- `gateway.upstream_response_read_max_bytes`:限制非流式上游响应读取大小(默认 `8MB`),用于防止异常响应导致内存放大。
|
||||||
|
- `gateway.proxy_probe_response_read_max_bytes`:限制代理探测响应读取大小(默认 `1MB`)。
|
||||||
|
- `gateway.gemini_debug_response_headers`:默认 `false`,仅在排障时短时开启,避免高频请求日志开销。
|
||||||
|
- `/auth/register`、`/auth/login`、`/auth/login/2fa`、`/auth/send-verify-code` 已提供服务端兜底限流(Redis 故障时 fail-close)。
|
||||||
|
- 推荐将 WAF/CDN 作为第一层防护,服务端限流与响应读取上限作为第二层兜底;两层同时保留,避免旁路流量与误配置风险。
|
||||||
|
|
||||||
**⚠️ 安全警告:HTTP URL 配置**
|
**⚠️ 安全警告:HTTP URL 配置**
|
||||||
|
|
||||||
当 `security.url_allowlist.enabled=false` 时,系统默认执行最小 URL 校验,**拒绝 HTTP URL**,仅允许 HTTPS。要允许 HTTP URL(例如用于开发或内网测试),必须显式设置:
|
当 `security.url_allowlist.enabled=false` 时,系统默认执行最小 URL 校验,**拒绝 HTTP URL**,仅允许 HTTPS。要允许 HTTP URL(例如用于开发或内网测试),必须显式设置:
|
||||||
@@ -428,6 +465,29 @@ Invalid base URL: invalid url scheme: http
|
|||||||
./sub2api
|
./sub2api
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### HTTP/2 (h2c) 与 HTTP/1.1 回退
|
||||||
|
|
||||||
|
后端明文端口默认支持 h2c,并保留 HTTP/1.1 回退用于 WebSocket 与旧客户端。浏览器通常不支持 h2c,性能收益主要在反向代理或内网链路。
|
||||||
|
|
||||||
|
**反向代理示例(Caddy):**
|
||||||
|
|
||||||
|
```caddyfile
|
||||||
|
transport http {
|
||||||
|
versions h2c h1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# h2c prior knowledge
|
||||||
|
curl --http2-prior-knowledge -I http://localhost:8080/health
|
||||||
|
# HTTP/1.1 回退
|
||||||
|
curl --http1.1 -I http://localhost:8080/health
|
||||||
|
# WebSocket 回退验证(需管理员 token)
|
||||||
|
websocat -H="Sec-WebSocket-Protocol: sub2api-admin, jwt.<ADMIN_TOKEN>" ws://localhost:8080/api/v1/admin/ops/ws/qps
|
||||||
|
```
|
||||||
|
|
||||||
#### 开发模式
|
#### 开发模式
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
5
backend/.gosec.json
Normal file
5
backend/.gosec.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"exclude": "G704"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,4 +14,7 @@ test-integration:
|
|||||||
go test -tags=integration ./...
|
go test -tags=integration ./...
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
go test -tags=e2e ./...
|
./scripts/e2e-test.sh
|
||||||
|
|
||||||
|
test-e2e-local:
|
||||||
|
go test -tags=e2e -v -timeout=300s ./internal/integration/...
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func main() {
|
|||||||
email := flag.String("email", "", "Admin email to issue a JWT for (defaults to first active admin)")
|
email := flag.String("email", "", "Admin email to issue a JWT for (defaults to first active admin)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
cfg, err := config.Load()
|
cfg, err := config.LoadForBootstrap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed to load config: %v", err)
|
log.Fatalf("failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.1.76
|
0.1.85
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -19,11 +18,14 @@ import (
|
|||||||
_ "github.com/Wei-Shaw/sub2api/ent/runtime"
|
_ "github.com/Wei-Shaw/sub2api/ent/runtime"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/handler"
|
"github.com/Wei-Shaw/sub2api/internal/handler"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/setup"
|
"github.com/Wei-Shaw/sub2api/internal/setup"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/web"
|
"github.com/Wei-Shaw/sub2api/internal/web"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed VERSION
|
//go:embed VERSION
|
||||||
@@ -38,7 +40,12 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Read version from embedded VERSION file
|
// 如果 Version 已通过 ldflags 注入(例如 -X main.Version=...),则不要覆盖。
|
||||||
|
if strings.TrimSpace(Version) != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认从 embedded VERSION 文件读取版本号(编译期打包进二进制)。
|
||||||
Version = strings.TrimSpace(embeddedVersion)
|
Version = strings.TrimSpace(embeddedVersion)
|
||||||
if Version == "" {
|
if Version == "" {
|
||||||
Version = "0.0.0-dev"
|
Version = "0.0.0-dev"
|
||||||
@@ -47,22 +54,9 @@ func init() {
|
|||||||
|
|
||||||
// initLogger configures the default slog handler based on gin.Mode().
|
// initLogger configures the default slog handler based on gin.Mode().
|
||||||
// In non-release mode, Debug level logs are enabled.
|
// In non-release mode, Debug level logs are enabled.
|
||||||
func initLogger() {
|
|
||||||
var level slog.Level
|
|
||||||
if gin.Mode() == gin.ReleaseMode {
|
|
||||||
level = slog.LevelInfo
|
|
||||||
} else {
|
|
||||||
level = slog.LevelDebug
|
|
||||||
}
|
|
||||||
handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
||||||
Level: level,
|
|
||||||
})
|
|
||||||
slog.SetDefault(slog.New(handler))
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Initialize slog logger based on gin mode
|
logger.InitBootstrap()
|
||||||
initLogger()
|
defer logger.Sync()
|
||||||
|
|
||||||
// Parse command line flags
|
// Parse command line flags
|
||||||
setupMode := flag.Bool("setup", false, "Run setup wizard in CLI mode")
|
setupMode := flag.Bool("setup", false, "Run setup wizard in CLI mode")
|
||||||
@@ -122,16 +116,26 @@ func runSetupServer() {
|
|||||||
log.Printf("Setup wizard available at http://%s", addr)
|
log.Printf("Setup wizard available at http://%s", addr)
|
||||||
log.Println("Complete the setup wizard to configure Sub2API")
|
log.Println("Complete the setup wizard to configure Sub2API")
|
||||||
|
|
||||||
if err := r.Run(addr); err != nil {
|
server := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: h2c.NewHandler(r, &http2.Server{}),
|
||||||
|
ReadHeaderTimeout: 30 * time.Second,
|
||||||
|
IdleTimeout: 120 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Fatalf("Failed to start setup server: %v", err)
|
log.Fatalf("Failed to start setup server: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMainServer() {
|
func runMainServer() {
|
||||||
cfg, err := config.Load()
|
cfg, err := config.LoadForBootstrap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := logger.Init(logger.OptionsFromConfig(cfg.Log)); err != nil {
|
||||||
|
log.Fatalf("Failed to initialize logger: %v", err)
|
||||||
|
}
|
||||||
if cfg.RunMode == config.RunModeSimple {
|
if cfg.RunMode == config.RunModeSimple {
|
||||||
log.Println("⚠️ WARNING: Running in SIMPLE mode - billing and quota checks are DISABLED")
|
log.Println("⚠️ WARNING: Running in SIMPLE mode - billing and quota checks are DISABLED")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,14 +67,19 @@ func provideCleanup(
|
|||||||
opsAlertEvaluator *service.OpsAlertEvaluatorService,
|
opsAlertEvaluator *service.OpsAlertEvaluatorService,
|
||||||
opsCleanup *service.OpsCleanupService,
|
opsCleanup *service.OpsCleanupService,
|
||||||
opsScheduledReport *service.OpsScheduledReportService,
|
opsScheduledReport *service.OpsScheduledReportService,
|
||||||
|
opsSystemLogSink *service.OpsSystemLogSink,
|
||||||
|
soraMediaCleanup *service.SoraMediaCleanupService,
|
||||||
schedulerSnapshot *service.SchedulerSnapshotService,
|
schedulerSnapshot *service.SchedulerSnapshotService,
|
||||||
tokenRefresh *service.TokenRefreshService,
|
tokenRefresh *service.TokenRefreshService,
|
||||||
accountExpiry *service.AccountExpiryService,
|
accountExpiry *service.AccountExpiryService,
|
||||||
subscriptionExpiry *service.SubscriptionExpiryService,
|
subscriptionExpiry *service.SubscriptionExpiryService,
|
||||||
usageCleanup *service.UsageCleanupService,
|
usageCleanup *service.UsageCleanupService,
|
||||||
|
idempotencyCleanup *service.IdempotencyCleanupService,
|
||||||
pricing *service.PricingService,
|
pricing *service.PricingService,
|
||||||
emailQueue *service.EmailQueueService,
|
emailQueue *service.EmailQueueService,
|
||||||
billingCache *service.BillingCacheService,
|
billingCache *service.BillingCacheService,
|
||||||
|
usageRecordWorkerPool *service.UsageRecordWorkerPool,
|
||||||
|
subscriptionService *service.SubscriptionService,
|
||||||
oauth *service.OAuthService,
|
oauth *service.OAuthService,
|
||||||
openaiOAuth *service.OpenAIOAuthService,
|
openaiOAuth *service.OpenAIOAuthService,
|
||||||
geminiOAuth *service.GeminiOAuthService,
|
geminiOAuth *service.GeminiOAuthService,
|
||||||
@@ -101,6 +106,18 @@ func provideCleanup(
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"OpsSystemLogSink", func() error {
|
||||||
|
if opsSystemLogSink != nil {
|
||||||
|
opsSystemLogSink.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{"SoraMediaCleanupService", func() error {
|
||||||
|
if soraMediaCleanup != nil {
|
||||||
|
soraMediaCleanup.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"OpsAlertEvaluatorService", func() error {
|
{"OpsAlertEvaluatorService", func() error {
|
||||||
if opsAlertEvaluator != nil {
|
if opsAlertEvaluator != nil {
|
||||||
opsAlertEvaluator.Stop()
|
opsAlertEvaluator.Stop()
|
||||||
@@ -131,6 +148,12 @@ func provideCleanup(
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"IdempotencyCleanupService", func() error {
|
||||||
|
if idempotencyCleanup != nil {
|
||||||
|
idempotencyCleanup.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"TokenRefreshService", func() error {
|
{"TokenRefreshService", func() error {
|
||||||
tokenRefresh.Stop()
|
tokenRefresh.Stop()
|
||||||
return nil
|
return nil
|
||||||
@@ -143,6 +166,12 @@ func provideCleanup(
|
|||||||
subscriptionExpiry.Stop()
|
subscriptionExpiry.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"SubscriptionService", func() error {
|
||||||
|
if subscriptionService != nil {
|
||||||
|
subscriptionService.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"PricingService", func() error {
|
{"PricingService", func() error {
|
||||||
pricing.Stop()
|
pricing.Stop()
|
||||||
return nil
|
return nil
|
||||||
@@ -155,6 +184,12 @@ func provideCleanup(
|
|||||||
billingCache.Stop()
|
billingCache.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"UsageRecordWorkerPool", func() error {
|
||||||
|
if usageRecordWorkerPool != nil {
|
||||||
|
usageRecordWorkerPool.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"OAuthService", func() error {
|
{"OAuthService", func() error {
|
||||||
oauth.Stop()
|
oauth.Stop()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
apiKeyAuthCacheInvalidator := service.ProvideAPIKeyAuthCacheInvalidator(apiKeyService)
|
apiKeyAuthCacheInvalidator := service.ProvideAPIKeyAuthCacheInvalidator(apiKeyService)
|
||||||
promoService := service.NewPromoService(promoCodeRepository, userRepository, billingCacheService, client, apiKeyAuthCacheInvalidator)
|
promoService := service.NewPromoService(promoCodeRepository, userRepository, billingCacheService, client, apiKeyAuthCacheInvalidator)
|
||||||
authService := service.NewAuthService(userRepository, redeemCodeRepository, refreshTokenCache, configConfig, settingService, emailService, turnstileService, emailQueueService, promoService)
|
authService := service.NewAuthService(userRepository, redeemCodeRepository, refreshTokenCache, configConfig, settingService, emailService, turnstileService, emailQueueService, promoService)
|
||||||
userService := service.NewUserService(userRepository, apiKeyAuthCacheInvalidator)
|
userService := service.NewUserService(userRepository, apiKeyAuthCacheInvalidator, billingCache)
|
||||||
subscriptionService := service.NewSubscriptionService(groupRepository, userSubscriptionRepository, billingCacheService)
|
subscriptionService := service.NewSubscriptionService(groupRepository, userSubscriptionRepository, billingCacheService, client, configConfig)
|
||||||
redeemCache := repository.NewRedeemCache(redisClient)
|
redeemCache := repository.NewRedeemCache(redisClient)
|
||||||
redeemService := service.NewRedeemService(redeemCodeRepository, userRepository, subscriptionService, redeemCache, billingCacheService, client, apiKeyAuthCacheInvalidator)
|
redeemService := service.NewRedeemService(redeemCodeRepository, userRepository, subscriptionService, redeemCache, billingCacheService, client, apiKeyAuthCacheInvalidator)
|
||||||
secretEncryptor, err := repository.NewAESEncryptor(configConfig)
|
secretEncryptor, err := repository.NewAESEncryptor(configConfig)
|
||||||
@@ -98,10 +98,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
dashboardHandler := admin.NewDashboardHandler(dashboardService, dashboardAggregationService)
|
dashboardHandler := admin.NewDashboardHandler(dashboardService, dashboardAggregationService)
|
||||||
schedulerCache := repository.NewSchedulerCache(redisClient)
|
schedulerCache := repository.NewSchedulerCache(redisClient)
|
||||||
accountRepository := repository.NewAccountRepository(client, db, schedulerCache)
|
accountRepository := repository.NewAccountRepository(client, db, schedulerCache)
|
||||||
|
soraAccountRepository := repository.NewSoraAccountRepository(db)
|
||||||
proxyRepository := repository.NewProxyRepository(client, db)
|
proxyRepository := repository.NewProxyRepository(client, db)
|
||||||
proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
|
proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
|
||||||
proxyLatencyCache := repository.NewProxyLatencyCache(redisClient)
|
proxyLatencyCache := repository.NewProxyLatencyCache(redisClient)
|
||||||
adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator)
|
adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, soraAccountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator)
|
||||||
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
|
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
|
||||||
concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig)
|
concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig)
|
||||||
adminUserHandler := admin.NewUserHandler(adminService, concurrencyService)
|
adminUserHandler := admin.NewUserHandler(adminService, concurrencyService)
|
||||||
@@ -112,7 +113,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
openAIOAuthService := service.NewOpenAIOAuthService(proxyRepository, openAIOAuthClient)
|
openAIOAuthService := service.NewOpenAIOAuthService(proxyRepository, openAIOAuthClient)
|
||||||
geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig)
|
geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig)
|
||||||
geminiCliCodeAssistClient := repository.NewGeminiCliCodeAssistClient()
|
geminiCliCodeAssistClient := repository.NewGeminiCliCodeAssistClient()
|
||||||
geminiOAuthService := service.NewGeminiOAuthService(proxyRepository, geminiOAuthClient, geminiCliCodeAssistClient, configConfig)
|
driveClient := repository.NewGeminiDriveClient()
|
||||||
|
geminiOAuthService := service.NewGeminiOAuthService(proxyRepository, geminiOAuthClient, geminiCliCodeAssistClient, driveClient, configConfig)
|
||||||
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
|
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
|
||||||
geminiQuotaService := service.NewGeminiQuotaService(configConfig, settingRepository)
|
geminiQuotaService := service.NewGeminiQuotaService(configConfig, settingRepository)
|
||||||
tempUnschedCache := repository.NewTempUnschedCache(redisClient)
|
tempUnschedCache := repository.NewTempUnschedCache(redisClient)
|
||||||
@@ -159,14 +161,17 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
openAITokenProvider := service.NewOpenAITokenProvider(accountRepository, geminiTokenCache, openAIOAuthService)
|
openAITokenProvider := service.NewOpenAITokenProvider(accountRepository, geminiTokenCache, openAIOAuthService)
|
||||||
openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService, openAITokenProvider)
|
openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService, openAITokenProvider)
|
||||||
geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, groupRepository, gatewayCache, schedulerSnapshotService, geminiTokenProvider, rateLimitService, httpUpstream, antigravityGatewayService, configConfig)
|
geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, groupRepository, gatewayCache, schedulerSnapshotService, geminiTokenProvider, rateLimitService, httpUpstream, antigravityGatewayService, configConfig)
|
||||||
opsService := service.NewOpsService(opsRepository, settingRepository, configConfig, accountRepository, userRepository, concurrencyService, gatewayService, openAIGatewayService, geminiMessagesCompatService, antigravityGatewayService)
|
opsSystemLogSink := service.ProvideOpsSystemLogSink(opsRepository)
|
||||||
|
opsService := service.NewOpsService(opsRepository, settingRepository, configConfig, accountRepository, userRepository, concurrencyService, gatewayService, openAIGatewayService, geminiMessagesCompatService, antigravityGatewayService, opsSystemLogSink)
|
||||||
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService)
|
settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService)
|
||||||
opsHandler := admin.NewOpsHandler(opsService)
|
opsHandler := admin.NewOpsHandler(opsService)
|
||||||
updateCache := repository.NewUpdateCache(redisClient)
|
updateCache := repository.NewUpdateCache(redisClient)
|
||||||
gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig)
|
gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig)
|
||||||
serviceBuildInfo := provideServiceBuildInfo(buildInfo)
|
serviceBuildInfo := provideServiceBuildInfo(buildInfo)
|
||||||
updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo)
|
updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo)
|
||||||
systemHandler := handler.ProvideSystemHandler(updateService)
|
idempotencyRepository := repository.NewIdempotencyRepository(client, db)
|
||||||
|
systemOperationLockService := service.ProvideSystemOperationLockService(idempotencyRepository, configConfig)
|
||||||
|
systemHandler := handler.ProvideSystemHandler(updateService, systemOperationLockService)
|
||||||
adminSubscriptionHandler := admin.NewSubscriptionHandler(subscriptionService)
|
adminSubscriptionHandler := admin.NewSubscriptionHandler(subscriptionService)
|
||||||
usageCleanupRepository := repository.NewUsageCleanupRepository(client, db)
|
usageCleanupRepository := repository.NewUsageCleanupRepository(client, db)
|
||||||
usageCleanupService := service.ProvideUsageCleanupService(usageCleanupRepository, timingWheelService, dashboardAggregationService, configConfig)
|
usageCleanupService := service.ProvideUsageCleanupService(usageCleanupRepository, timingWheelService, dashboardAggregationService, configConfig)
|
||||||
@@ -180,11 +185,18 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
errorPassthroughService := service.NewErrorPassthroughService(errorPassthroughRepository, errorPassthroughCache)
|
errorPassthroughService := service.NewErrorPassthroughService(errorPassthroughRepository, errorPassthroughCache)
|
||||||
errorPassthroughHandler := admin.NewErrorPassthroughHandler(errorPassthroughService)
|
errorPassthroughHandler := admin.NewErrorPassthroughHandler(errorPassthroughService)
|
||||||
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler)
|
adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler)
|
||||||
gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, usageService, apiKeyService, errorPassthroughService, configConfig)
|
usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig)
|
||||||
openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService, apiKeyService, errorPassthroughService, configConfig)
|
gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, usageService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, configConfig)
|
||||||
|
openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, configConfig)
|
||||||
|
soraSDKClient := service.ProvideSoraSDKClient(configConfig, httpUpstream, openAITokenProvider, accountRepository, soraAccountRepository)
|
||||||
|
soraMediaStorage := service.ProvideSoraMediaStorage(configConfig)
|
||||||
|
soraGatewayService := service.NewSoraGatewayService(soraSDKClient, soraMediaStorage, rateLimitService, configConfig)
|
||||||
|
soraGatewayHandler := handler.NewSoraGatewayHandler(gatewayService, soraGatewayService, concurrencyService, billingCacheService, usageRecordWorkerPool, configConfig)
|
||||||
handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo)
|
handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo)
|
||||||
totpHandler := handler.NewTotpHandler(totpService)
|
totpHandler := handler.NewTotpHandler(totpService)
|
||||||
handlers := handler.ProvideHandlers(authHandler, userHandler, apiKeyHandler, usageHandler, redeemHandler, subscriptionHandler, announcementHandler, adminHandlers, gatewayHandler, openAIGatewayHandler, handlerSettingHandler, totpHandler)
|
idempotencyCoordinator := service.ProvideIdempotencyCoordinator(idempotencyRepository, configConfig)
|
||||||
|
idempotencyCleanupService := service.ProvideIdempotencyCleanupService(idempotencyRepository, configConfig)
|
||||||
|
handlers := handler.ProvideHandlers(authHandler, userHandler, apiKeyHandler, usageHandler, redeemHandler, subscriptionHandler, announcementHandler, adminHandlers, gatewayHandler, openAIGatewayHandler, soraGatewayHandler, handlerSettingHandler, totpHandler, idempotencyCoordinator, idempotencyCleanupService)
|
||||||
jwtAuthMiddleware := middleware.NewJWTAuthMiddleware(authService, userService)
|
jwtAuthMiddleware := middleware.NewJWTAuthMiddleware(authService, userService)
|
||||||
adminAuthMiddleware := middleware.NewAdminAuthMiddleware(authService, userService, settingService)
|
adminAuthMiddleware := middleware.NewAdminAuthMiddleware(authService, userService, settingService)
|
||||||
apiKeyAuthMiddleware := middleware.NewAPIKeyAuthMiddleware(apiKeyService, subscriptionService, configConfig)
|
apiKeyAuthMiddleware := middleware.NewAPIKeyAuthMiddleware(apiKeyService, subscriptionService, configConfig)
|
||||||
@@ -195,10 +207,11 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
opsAlertEvaluatorService := service.ProvideOpsAlertEvaluatorService(opsService, opsRepository, emailService, redisClient, configConfig)
|
opsAlertEvaluatorService := service.ProvideOpsAlertEvaluatorService(opsService, opsRepository, emailService, redisClient, configConfig)
|
||||||
opsCleanupService := service.ProvideOpsCleanupService(opsRepository, db, redisClient, configConfig)
|
opsCleanupService := service.ProvideOpsCleanupService(opsRepository, db, redisClient, configConfig)
|
||||||
opsScheduledReportService := service.ProvideOpsScheduledReportService(opsService, userService, emailService, redisClient, configConfig)
|
opsScheduledReportService := service.ProvideOpsScheduledReportService(opsService, userService, emailService, redisClient, configConfig)
|
||||||
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, compositeTokenCacheInvalidator, schedulerCache, configConfig)
|
soraMediaCleanupService := service.ProvideSoraMediaCleanupService(soraMediaStorage, configConfig)
|
||||||
|
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, soraAccountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, compositeTokenCacheInvalidator, schedulerCache, configConfig)
|
||||||
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
|
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
|
||||||
subscriptionExpiryService := service.ProvideSubscriptionExpiryService(userSubscriptionRepository)
|
subscriptionExpiryService := service.ProvideSubscriptionExpiryService(userSubscriptionRepository)
|
||||||
v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, schedulerSnapshotService, tokenRefreshService, accountExpiryService, subscriptionExpiryService, usageCleanupService, pricingService, emailQueueService, billingCacheService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
|
v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, opsSystemLogSink, soraMediaCleanupService, schedulerSnapshotService, tokenRefreshService, accountExpiryService, subscriptionExpiryService, usageCleanupService, idempotencyCleanupService, pricingService, emailQueueService, billingCacheService, usageRecordWorkerPool, subscriptionService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
|
||||||
application := &Application{
|
application := &Application{
|
||||||
Server: httpServer,
|
Server: httpServer,
|
||||||
Cleanup: v,
|
Cleanup: v,
|
||||||
@@ -228,14 +241,19 @@ func provideCleanup(
|
|||||||
opsAlertEvaluator *service.OpsAlertEvaluatorService,
|
opsAlertEvaluator *service.OpsAlertEvaluatorService,
|
||||||
opsCleanup *service.OpsCleanupService,
|
opsCleanup *service.OpsCleanupService,
|
||||||
opsScheduledReport *service.OpsScheduledReportService,
|
opsScheduledReport *service.OpsScheduledReportService,
|
||||||
|
opsSystemLogSink *service.OpsSystemLogSink,
|
||||||
|
soraMediaCleanup *service.SoraMediaCleanupService,
|
||||||
schedulerSnapshot *service.SchedulerSnapshotService,
|
schedulerSnapshot *service.SchedulerSnapshotService,
|
||||||
tokenRefresh *service.TokenRefreshService,
|
tokenRefresh *service.TokenRefreshService,
|
||||||
accountExpiry *service.AccountExpiryService,
|
accountExpiry *service.AccountExpiryService,
|
||||||
subscriptionExpiry *service.SubscriptionExpiryService,
|
subscriptionExpiry *service.SubscriptionExpiryService,
|
||||||
usageCleanup *service.UsageCleanupService,
|
usageCleanup *service.UsageCleanupService,
|
||||||
|
idempotencyCleanup *service.IdempotencyCleanupService,
|
||||||
pricing *service.PricingService,
|
pricing *service.PricingService,
|
||||||
emailQueue *service.EmailQueueService,
|
emailQueue *service.EmailQueueService,
|
||||||
billingCache *service.BillingCacheService,
|
billingCache *service.BillingCacheService,
|
||||||
|
usageRecordWorkerPool *service.UsageRecordWorkerPool,
|
||||||
|
subscriptionService *service.SubscriptionService,
|
||||||
oauth *service.OAuthService,
|
oauth *service.OAuthService,
|
||||||
openaiOAuth *service.OpenAIOAuthService,
|
openaiOAuth *service.OpenAIOAuthService,
|
||||||
geminiOAuth *service.GeminiOAuthService,
|
geminiOAuth *service.GeminiOAuthService,
|
||||||
@@ -261,6 +279,18 @@ func provideCleanup(
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"OpsSystemLogSink", func() error {
|
||||||
|
if opsSystemLogSink != nil {
|
||||||
|
opsSystemLogSink.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
{"SoraMediaCleanupService", func() error {
|
||||||
|
if soraMediaCleanup != nil {
|
||||||
|
soraMediaCleanup.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"OpsAlertEvaluatorService", func() error {
|
{"OpsAlertEvaluatorService", func() error {
|
||||||
if opsAlertEvaluator != nil {
|
if opsAlertEvaluator != nil {
|
||||||
opsAlertEvaluator.Stop()
|
opsAlertEvaluator.Stop()
|
||||||
@@ -291,6 +321,12 @@ func provideCleanup(
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"IdempotencyCleanupService", func() error {
|
||||||
|
if idempotencyCleanup != nil {
|
||||||
|
idempotencyCleanup.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"TokenRefreshService", func() error {
|
{"TokenRefreshService", func() error {
|
||||||
tokenRefresh.Stop()
|
tokenRefresh.Stop()
|
||||||
return nil
|
return nil
|
||||||
@@ -303,6 +339,12 @@ func provideCleanup(
|
|||||||
subscriptionExpiry.Stop()
|
subscriptionExpiry.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"SubscriptionService", func() error {
|
||||||
|
if subscriptionService != nil {
|
||||||
|
subscriptionService.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"PricingService", func() error {
|
{"PricingService", func() error {
|
||||||
pricing.Stop()
|
pricing.Stop()
|
||||||
return nil
|
return nil
|
||||||
@@ -315,6 +357,12 @@ func provideCleanup(
|
|||||||
billingCache.Stop()
|
billingCache.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"UsageRecordWorkerPool", func() error {
|
||||||
|
if usageRecordWorkerPool != nil {
|
||||||
|
usageRecordWorkerPool.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"OAuthService", func() error {
|
{"OAuthService", func() error {
|
||||||
oauth.Stop()
|
oauth.Stop()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ type APIKey struct {
|
|||||||
GroupID *int64 `json:"group_id,omitempty"`
|
GroupID *int64 `json:"group_id,omitempty"`
|
||||||
// Status holds the value of the "status" field.
|
// Status holds the value of the "status" field.
|
||||||
Status string `json:"status,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
|
// Last usage time of this API key
|
||||||
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||||
// Allowed IPs/CIDRs, e.g. ["192.168.1.100", "10.0.0.0/8"]
|
// Allowed IPs/CIDRs, e.g. ["192.168.1.100", "10.0.0.0/8"]
|
||||||
IPWhitelist []string `json:"ip_whitelist,omitempty"`
|
IPWhitelist []string `json:"ip_whitelist,omitempty"`
|
||||||
// Blocked IPs/CIDRs
|
// Blocked IPs/CIDRs
|
||||||
@@ -109,7 +111,7 @@ func (*APIKey) scanValues(columns []string) ([]any, error) {
|
|||||||
values[i] = new(sql.NullInt64)
|
values[i] = new(sql.NullInt64)
|
||||||
case apikey.FieldKey, apikey.FieldName, apikey.FieldStatus:
|
case apikey.FieldKey, apikey.FieldName, apikey.FieldStatus:
|
||||||
values[i] = new(sql.NullString)
|
values[i] = new(sql.NullString)
|
||||||
case apikey.FieldCreatedAt, apikey.FieldUpdatedAt, apikey.FieldDeletedAt, apikey.FieldExpiresAt:
|
case apikey.FieldCreatedAt, apikey.FieldUpdatedAt, apikey.FieldDeletedAt, apikey.FieldLastUsedAt, apikey.FieldExpiresAt:
|
||||||
values[i] = new(sql.NullTime)
|
values[i] = new(sql.NullTime)
|
||||||
default:
|
default:
|
||||||
values[i] = new(sql.UnknownType)
|
values[i] = new(sql.UnknownType)
|
||||||
@@ -182,6 +184,13 @@ func (_m *APIKey) assignValues(columns []string, values []any) error {
|
|||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.Status = value.String
|
_m.Status = value.String
|
||||||
}
|
}
|
||||||
|
case apikey.FieldLastUsedAt:
|
||||||
|
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field last_used_at", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.LastUsedAt = new(time.Time)
|
||||||
|
*_m.LastUsedAt = value.Time
|
||||||
|
}
|
||||||
case apikey.FieldIPWhitelist:
|
case apikey.FieldIPWhitelist:
|
||||||
if value, ok := values[i].(*[]byte); !ok {
|
if value, ok := values[i].(*[]byte); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field ip_whitelist", values[i])
|
return fmt.Errorf("unexpected type %T for field ip_whitelist", values[i])
|
||||||
@@ -296,6 +305,11 @@ func (_m *APIKey) String() string {
|
|||||||
builder.WriteString("status=")
|
builder.WriteString("status=")
|
||||||
builder.WriteString(_m.Status)
|
builder.WriteString(_m.Status)
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
if v := _m.LastUsedAt; v != nil {
|
||||||
|
builder.WriteString("last_used_at=")
|
||||||
|
builder.WriteString(v.Format(time.ANSIC))
|
||||||
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
builder.WriteString("ip_whitelist=")
|
builder.WriteString("ip_whitelist=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", _m.IPWhitelist))
|
builder.WriteString(fmt.Sprintf("%v", _m.IPWhitelist))
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ const (
|
|||||||
FieldGroupID = "group_id"
|
FieldGroupID = "group_id"
|
||||||
// FieldStatus holds the string denoting the status field in the database.
|
// FieldStatus holds the string denoting the status field in the database.
|
||||||
FieldStatus = "status"
|
FieldStatus = "status"
|
||||||
|
// FieldLastUsedAt holds the string denoting the last_used_at field in the database.
|
||||||
|
FieldLastUsedAt = "last_used_at"
|
||||||
// FieldIPWhitelist holds the string denoting the ip_whitelist field in the database.
|
// FieldIPWhitelist holds the string denoting the ip_whitelist field in the database.
|
||||||
FieldIPWhitelist = "ip_whitelist"
|
FieldIPWhitelist = "ip_whitelist"
|
||||||
// FieldIPBlacklist holds the string denoting the ip_blacklist field in the database.
|
// FieldIPBlacklist holds the string denoting the ip_blacklist field in the database.
|
||||||
@@ -83,6 +85,7 @@ var Columns = []string{
|
|||||||
FieldName,
|
FieldName,
|
||||||
FieldGroupID,
|
FieldGroupID,
|
||||||
FieldStatus,
|
FieldStatus,
|
||||||
|
FieldLastUsedAt,
|
||||||
FieldIPWhitelist,
|
FieldIPWhitelist,
|
||||||
FieldIPBlacklist,
|
FieldIPBlacklist,
|
||||||
FieldQuota,
|
FieldQuota,
|
||||||
@@ -176,6 +179,11 @@ func ByStatus(opts ...sql.OrderTermOption) OrderOption {
|
|||||||
return sql.OrderByField(FieldStatus, opts...).ToFunc()
|
return sql.OrderByField(FieldStatus, opts...).ToFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ByLastUsedAt orders the results by the last_used_at field.
|
||||||
|
func ByLastUsedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldLastUsedAt, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
// ByQuota orders the results by the quota field.
|
// ByQuota orders the results by the quota field.
|
||||||
func ByQuota(opts ...sql.OrderTermOption) OrderOption {
|
func ByQuota(opts ...sql.OrderTermOption) OrderOption {
|
||||||
return sql.OrderByField(FieldQuota, opts...).ToFunc()
|
return sql.OrderByField(FieldQuota, opts...).ToFunc()
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ func Status(v string) predicate.APIKey {
|
|||||||
return predicate.APIKey(sql.FieldEQ(FieldStatus, v))
|
return predicate.APIKey(sql.FieldEQ(FieldStatus, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LastUsedAt applies equality check predicate on the "last_used_at" field. It's identical to LastUsedAtEQ.
|
||||||
|
func LastUsedAt(v time.Time) predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldEQ(FieldLastUsedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
// Quota applies equality check predicate on the "quota" field. It's identical to QuotaEQ.
|
// Quota applies equality check predicate on the "quota" field. It's identical to QuotaEQ.
|
||||||
func Quota(v float64) predicate.APIKey {
|
func Quota(v float64) predicate.APIKey {
|
||||||
return predicate.APIKey(sql.FieldEQ(FieldQuota, v))
|
return predicate.APIKey(sql.FieldEQ(FieldQuota, v))
|
||||||
@@ -485,6 +490,56 @@ func StatusContainsFold(v string) predicate.APIKey {
|
|||||||
return predicate.APIKey(sql.FieldContainsFold(FieldStatus, v))
|
return predicate.APIKey(sql.FieldContainsFold(FieldStatus, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LastUsedAtEQ applies the EQ predicate on the "last_used_at" field.
|
||||||
|
func LastUsedAtEQ(v time.Time) predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldEQ(FieldLastUsedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastUsedAtNEQ applies the NEQ predicate on the "last_used_at" field.
|
||||||
|
func LastUsedAtNEQ(v time.Time) predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldNEQ(FieldLastUsedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastUsedAtIn applies the In predicate on the "last_used_at" field.
|
||||||
|
func LastUsedAtIn(vs ...time.Time) predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldIn(FieldLastUsedAt, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastUsedAtNotIn applies the NotIn predicate on the "last_used_at" field.
|
||||||
|
func LastUsedAtNotIn(vs ...time.Time) predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldNotIn(FieldLastUsedAt, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastUsedAtGT applies the GT predicate on the "last_used_at" field.
|
||||||
|
func LastUsedAtGT(v time.Time) predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldGT(FieldLastUsedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastUsedAtGTE applies the GTE predicate on the "last_used_at" field.
|
||||||
|
func LastUsedAtGTE(v time.Time) predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldGTE(FieldLastUsedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastUsedAtLT applies the LT predicate on the "last_used_at" field.
|
||||||
|
func LastUsedAtLT(v time.Time) predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldLT(FieldLastUsedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastUsedAtLTE applies the LTE predicate on the "last_used_at" field.
|
||||||
|
func LastUsedAtLTE(v time.Time) predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldLTE(FieldLastUsedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastUsedAtIsNil applies the IsNil predicate on the "last_used_at" field.
|
||||||
|
func LastUsedAtIsNil() predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldIsNull(FieldLastUsedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastUsedAtNotNil applies the NotNil predicate on the "last_used_at" field.
|
||||||
|
func LastUsedAtNotNil() predicate.APIKey {
|
||||||
|
return predicate.APIKey(sql.FieldNotNull(FieldLastUsedAt))
|
||||||
|
}
|
||||||
|
|
||||||
// IPWhitelistIsNil applies the IsNil predicate on the "ip_whitelist" field.
|
// IPWhitelistIsNil applies the IsNil predicate on the "ip_whitelist" field.
|
||||||
func IPWhitelistIsNil() predicate.APIKey {
|
func IPWhitelistIsNil() predicate.APIKey {
|
||||||
return predicate.APIKey(sql.FieldIsNull(FieldIPWhitelist))
|
return predicate.APIKey(sql.FieldIsNull(FieldIPWhitelist))
|
||||||
|
|||||||
@@ -113,6 +113,20 @@ func (_c *APIKeyCreate) SetNillableStatus(v *string) *APIKeyCreate {
|
|||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLastUsedAt sets the "last_used_at" field.
|
||||||
|
func (_c *APIKeyCreate) SetLastUsedAt(v time.Time) *APIKeyCreate {
|
||||||
|
_c.mutation.SetLastUsedAt(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableLastUsedAt sets the "last_used_at" field if the given value is not nil.
|
||||||
|
func (_c *APIKeyCreate) SetNillableLastUsedAt(v *time.Time) *APIKeyCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetLastUsedAt(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SetIPWhitelist sets the "ip_whitelist" field.
|
// SetIPWhitelist sets the "ip_whitelist" field.
|
||||||
func (_c *APIKeyCreate) SetIPWhitelist(v []string) *APIKeyCreate {
|
func (_c *APIKeyCreate) SetIPWhitelist(v []string) *APIKeyCreate {
|
||||||
_c.mutation.SetIPWhitelist(v)
|
_c.mutation.SetIPWhitelist(v)
|
||||||
@@ -353,6 +367,10 @@ func (_c *APIKeyCreate) createSpec() (*APIKey, *sqlgraph.CreateSpec) {
|
|||||||
_spec.SetField(apikey.FieldStatus, field.TypeString, value)
|
_spec.SetField(apikey.FieldStatus, field.TypeString, value)
|
||||||
_node.Status = value
|
_node.Status = value
|
||||||
}
|
}
|
||||||
|
if value, ok := _c.mutation.LastUsedAt(); ok {
|
||||||
|
_spec.SetField(apikey.FieldLastUsedAt, field.TypeTime, value)
|
||||||
|
_node.LastUsedAt = &value
|
||||||
|
}
|
||||||
if value, ok := _c.mutation.IPWhitelist(); ok {
|
if value, ok := _c.mutation.IPWhitelist(); ok {
|
||||||
_spec.SetField(apikey.FieldIPWhitelist, field.TypeJSON, value)
|
_spec.SetField(apikey.FieldIPWhitelist, field.TypeJSON, value)
|
||||||
_node.IPWhitelist = value
|
_node.IPWhitelist = value
|
||||||
@@ -571,6 +589,24 @@ func (u *APIKeyUpsert) UpdateStatus() *APIKeyUpsert {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLastUsedAt sets the "last_used_at" field.
|
||||||
|
func (u *APIKeyUpsert) SetLastUsedAt(v time.Time) *APIKeyUpsert {
|
||||||
|
u.Set(apikey.FieldLastUsedAt, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLastUsedAt sets the "last_used_at" field to the value that was provided on create.
|
||||||
|
func (u *APIKeyUpsert) UpdateLastUsedAt() *APIKeyUpsert {
|
||||||
|
u.SetExcluded(apikey.FieldLastUsedAt)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLastUsedAt clears the value of the "last_used_at" field.
|
||||||
|
func (u *APIKeyUpsert) ClearLastUsedAt() *APIKeyUpsert {
|
||||||
|
u.SetNull(apikey.FieldLastUsedAt)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
// SetIPWhitelist sets the "ip_whitelist" field.
|
// SetIPWhitelist sets the "ip_whitelist" field.
|
||||||
func (u *APIKeyUpsert) SetIPWhitelist(v []string) *APIKeyUpsert {
|
func (u *APIKeyUpsert) SetIPWhitelist(v []string) *APIKeyUpsert {
|
||||||
u.Set(apikey.FieldIPWhitelist, v)
|
u.Set(apikey.FieldIPWhitelist, v)
|
||||||
@@ -818,6 +854,27 @@ func (u *APIKeyUpsertOne) UpdateStatus() *APIKeyUpsertOne {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLastUsedAt sets the "last_used_at" field.
|
||||||
|
func (u *APIKeyUpsertOne) SetLastUsedAt(v time.Time) *APIKeyUpsertOne {
|
||||||
|
return u.Update(func(s *APIKeyUpsert) {
|
||||||
|
s.SetLastUsedAt(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLastUsedAt sets the "last_used_at" field to the value that was provided on create.
|
||||||
|
func (u *APIKeyUpsertOne) UpdateLastUsedAt() *APIKeyUpsertOne {
|
||||||
|
return u.Update(func(s *APIKeyUpsert) {
|
||||||
|
s.UpdateLastUsedAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLastUsedAt clears the value of the "last_used_at" field.
|
||||||
|
func (u *APIKeyUpsertOne) ClearLastUsedAt() *APIKeyUpsertOne {
|
||||||
|
return u.Update(func(s *APIKeyUpsert) {
|
||||||
|
s.ClearLastUsedAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetIPWhitelist sets the "ip_whitelist" field.
|
// SetIPWhitelist sets the "ip_whitelist" field.
|
||||||
func (u *APIKeyUpsertOne) SetIPWhitelist(v []string) *APIKeyUpsertOne {
|
func (u *APIKeyUpsertOne) SetIPWhitelist(v []string) *APIKeyUpsertOne {
|
||||||
return u.Update(func(s *APIKeyUpsert) {
|
return u.Update(func(s *APIKeyUpsert) {
|
||||||
@@ -1246,6 +1303,27 @@ func (u *APIKeyUpsertBulk) UpdateStatus() *APIKeyUpsertBulk {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLastUsedAt sets the "last_used_at" field.
|
||||||
|
func (u *APIKeyUpsertBulk) SetLastUsedAt(v time.Time) *APIKeyUpsertBulk {
|
||||||
|
return u.Update(func(s *APIKeyUpsert) {
|
||||||
|
s.SetLastUsedAt(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLastUsedAt sets the "last_used_at" field to the value that was provided on create.
|
||||||
|
func (u *APIKeyUpsertBulk) UpdateLastUsedAt() *APIKeyUpsertBulk {
|
||||||
|
return u.Update(func(s *APIKeyUpsert) {
|
||||||
|
s.UpdateLastUsedAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLastUsedAt clears the value of the "last_used_at" field.
|
||||||
|
func (u *APIKeyUpsertBulk) ClearLastUsedAt() *APIKeyUpsertBulk {
|
||||||
|
return u.Update(func(s *APIKeyUpsert) {
|
||||||
|
s.ClearLastUsedAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetIPWhitelist sets the "ip_whitelist" field.
|
// SetIPWhitelist sets the "ip_whitelist" field.
|
||||||
func (u *APIKeyUpsertBulk) SetIPWhitelist(v []string) *APIKeyUpsertBulk {
|
func (u *APIKeyUpsertBulk) SetIPWhitelist(v []string) *APIKeyUpsertBulk {
|
||||||
return u.Update(func(s *APIKeyUpsert) {
|
return u.Update(func(s *APIKeyUpsert) {
|
||||||
|
|||||||
@@ -134,6 +134,26 @@ func (_u *APIKeyUpdate) SetNillableStatus(v *string) *APIKeyUpdate {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLastUsedAt sets the "last_used_at" field.
|
||||||
|
func (_u *APIKeyUpdate) SetLastUsedAt(v time.Time) *APIKeyUpdate {
|
||||||
|
_u.mutation.SetLastUsedAt(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableLastUsedAt sets the "last_used_at" field if the given value is not nil.
|
||||||
|
func (_u *APIKeyUpdate) SetNillableLastUsedAt(v *time.Time) *APIKeyUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetLastUsedAt(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLastUsedAt clears the value of the "last_used_at" field.
|
||||||
|
func (_u *APIKeyUpdate) ClearLastUsedAt() *APIKeyUpdate {
|
||||||
|
_u.mutation.ClearLastUsedAt()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetIPWhitelist sets the "ip_whitelist" field.
|
// SetIPWhitelist sets the "ip_whitelist" field.
|
||||||
func (_u *APIKeyUpdate) SetIPWhitelist(v []string) *APIKeyUpdate {
|
func (_u *APIKeyUpdate) SetIPWhitelist(v []string) *APIKeyUpdate {
|
||||||
_u.mutation.SetIPWhitelist(v)
|
_u.mutation.SetIPWhitelist(v)
|
||||||
@@ -390,6 +410,12 @@ func (_u *APIKeyUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
|||||||
if value, ok := _u.mutation.Status(); ok {
|
if value, ok := _u.mutation.Status(); ok {
|
||||||
_spec.SetField(apikey.FieldStatus, field.TypeString, value)
|
_spec.SetField(apikey.FieldStatus, field.TypeString, value)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.LastUsedAt(); ok {
|
||||||
|
_spec.SetField(apikey.FieldLastUsedAt, field.TypeTime, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.LastUsedAtCleared() {
|
||||||
|
_spec.ClearField(apikey.FieldLastUsedAt, field.TypeTime)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.IPWhitelist(); ok {
|
if value, ok := _u.mutation.IPWhitelist(); ok {
|
||||||
_spec.SetField(apikey.FieldIPWhitelist, field.TypeJSON, value)
|
_spec.SetField(apikey.FieldIPWhitelist, field.TypeJSON, value)
|
||||||
}
|
}
|
||||||
@@ -655,6 +681,26 @@ func (_u *APIKeyUpdateOne) SetNillableStatus(v *string) *APIKeyUpdateOne {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLastUsedAt sets the "last_used_at" field.
|
||||||
|
func (_u *APIKeyUpdateOne) SetLastUsedAt(v time.Time) *APIKeyUpdateOne {
|
||||||
|
_u.mutation.SetLastUsedAt(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableLastUsedAt sets the "last_used_at" field if the given value is not nil.
|
||||||
|
func (_u *APIKeyUpdateOne) SetNillableLastUsedAt(v *time.Time) *APIKeyUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetLastUsedAt(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLastUsedAt clears the value of the "last_used_at" field.
|
||||||
|
func (_u *APIKeyUpdateOne) ClearLastUsedAt() *APIKeyUpdateOne {
|
||||||
|
_u.mutation.ClearLastUsedAt()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetIPWhitelist sets the "ip_whitelist" field.
|
// SetIPWhitelist sets the "ip_whitelist" field.
|
||||||
func (_u *APIKeyUpdateOne) SetIPWhitelist(v []string) *APIKeyUpdateOne {
|
func (_u *APIKeyUpdateOne) SetIPWhitelist(v []string) *APIKeyUpdateOne {
|
||||||
_u.mutation.SetIPWhitelist(v)
|
_u.mutation.SetIPWhitelist(v)
|
||||||
@@ -941,6 +987,12 @@ func (_u *APIKeyUpdateOne) sqlSave(ctx context.Context) (_node *APIKey, err erro
|
|||||||
if value, ok := _u.mutation.Status(); ok {
|
if value, ok := _u.mutation.Status(); ok {
|
||||||
_spec.SetField(apikey.FieldStatus, field.TypeString, value)
|
_spec.SetField(apikey.FieldStatus, field.TypeString, value)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.LastUsedAt(); ok {
|
||||||
|
_spec.SetField(apikey.FieldLastUsedAt, field.TypeTime, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.LastUsedAtCleared() {
|
||||||
|
_spec.ClearField(apikey.FieldLastUsedAt, field.TypeTime)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.IPWhitelist(); ok {
|
if value, ok := _u.mutation.IPWhitelist(); ok {
|
||||||
_spec.SetField(apikey.FieldIPWhitelist, field.TypeJSON, value)
|
_spec.SetField(apikey.FieldIPWhitelist, field.TypeJSON, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
|
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/proxy"
|
"github.com/Wei-Shaw/sub2api/ent/proxy"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
"github.com/Wei-Shaw/sub2api/ent/setting"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||||
@@ -65,6 +66,8 @@ type Client struct {
|
|||||||
Proxy *ProxyClient
|
Proxy *ProxyClient
|
||||||
// RedeemCode is the client for interacting with the RedeemCode builders.
|
// RedeemCode is the client for interacting with the RedeemCode builders.
|
||||||
RedeemCode *RedeemCodeClient
|
RedeemCode *RedeemCodeClient
|
||||||
|
// SecuritySecret is the client for interacting with the SecuritySecret builders.
|
||||||
|
SecuritySecret *SecuritySecretClient
|
||||||
// Setting is the client for interacting with the Setting builders.
|
// Setting is the client for interacting with the Setting builders.
|
||||||
Setting *SettingClient
|
Setting *SettingClient
|
||||||
// UsageCleanupTask is the client for interacting with the UsageCleanupTask builders.
|
// UsageCleanupTask is the client for interacting with the UsageCleanupTask builders.
|
||||||
@@ -103,6 +106,7 @@ func (c *Client) init() {
|
|||||||
c.PromoCodeUsage = NewPromoCodeUsageClient(c.config)
|
c.PromoCodeUsage = NewPromoCodeUsageClient(c.config)
|
||||||
c.Proxy = NewProxyClient(c.config)
|
c.Proxy = NewProxyClient(c.config)
|
||||||
c.RedeemCode = NewRedeemCodeClient(c.config)
|
c.RedeemCode = NewRedeemCodeClient(c.config)
|
||||||
|
c.SecuritySecret = NewSecuritySecretClient(c.config)
|
||||||
c.Setting = NewSettingClient(c.config)
|
c.Setting = NewSettingClient(c.config)
|
||||||
c.UsageCleanupTask = NewUsageCleanupTaskClient(c.config)
|
c.UsageCleanupTask = NewUsageCleanupTaskClient(c.config)
|
||||||
c.UsageLog = NewUsageLogClient(c.config)
|
c.UsageLog = NewUsageLogClient(c.config)
|
||||||
@@ -214,6 +218,7 @@ func (c *Client) Tx(ctx context.Context) (*Tx, error) {
|
|||||||
PromoCodeUsage: NewPromoCodeUsageClient(cfg),
|
PromoCodeUsage: NewPromoCodeUsageClient(cfg),
|
||||||
Proxy: NewProxyClient(cfg),
|
Proxy: NewProxyClient(cfg),
|
||||||
RedeemCode: NewRedeemCodeClient(cfg),
|
RedeemCode: NewRedeemCodeClient(cfg),
|
||||||
|
SecuritySecret: NewSecuritySecretClient(cfg),
|
||||||
Setting: NewSettingClient(cfg),
|
Setting: NewSettingClient(cfg),
|
||||||
UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
|
UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
|
||||||
UsageLog: NewUsageLogClient(cfg),
|
UsageLog: NewUsageLogClient(cfg),
|
||||||
@@ -252,6 +257,7 @@ func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
|
|||||||
PromoCodeUsage: NewPromoCodeUsageClient(cfg),
|
PromoCodeUsage: NewPromoCodeUsageClient(cfg),
|
||||||
Proxy: NewProxyClient(cfg),
|
Proxy: NewProxyClient(cfg),
|
||||||
RedeemCode: NewRedeemCodeClient(cfg),
|
RedeemCode: NewRedeemCodeClient(cfg),
|
||||||
|
SecuritySecret: NewSecuritySecretClient(cfg),
|
||||||
Setting: NewSettingClient(cfg),
|
Setting: NewSettingClient(cfg),
|
||||||
UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
|
UsageCleanupTask: NewUsageCleanupTaskClient(cfg),
|
||||||
UsageLog: NewUsageLogClient(cfg),
|
UsageLog: NewUsageLogClient(cfg),
|
||||||
@@ -291,8 +297,8 @@ func (c *Client) Use(hooks ...Hook) {
|
|||||||
for _, n := range []interface{ Use(...Hook) }{
|
for _, n := range []interface{ Use(...Hook) }{
|
||||||
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
|
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
|
||||||
c.ErrorPassthroughRule, c.Group, c.PromoCode, c.PromoCodeUsage, c.Proxy,
|
c.ErrorPassthroughRule, c.Group, c.PromoCode, c.PromoCodeUsage, c.Proxy,
|
||||||
c.RedeemCode, c.Setting, c.UsageCleanupTask, c.UsageLog, c.User,
|
c.RedeemCode, c.SecuritySecret, c.Setting, c.UsageCleanupTask, c.UsageLog,
|
||||||
c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue,
|
c.User, c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue,
|
||||||
c.UserSubscription,
|
c.UserSubscription,
|
||||||
} {
|
} {
|
||||||
n.Use(hooks...)
|
n.Use(hooks...)
|
||||||
@@ -305,8 +311,8 @@ func (c *Client) Intercept(interceptors ...Interceptor) {
|
|||||||
for _, n := range []interface{ Intercept(...Interceptor) }{
|
for _, n := range []interface{ Intercept(...Interceptor) }{
|
||||||
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
|
c.APIKey, c.Account, c.AccountGroup, c.Announcement, c.AnnouncementRead,
|
||||||
c.ErrorPassthroughRule, c.Group, c.PromoCode, c.PromoCodeUsage, c.Proxy,
|
c.ErrorPassthroughRule, c.Group, c.PromoCode, c.PromoCodeUsage, c.Proxy,
|
||||||
c.RedeemCode, c.Setting, c.UsageCleanupTask, c.UsageLog, c.User,
|
c.RedeemCode, c.SecuritySecret, c.Setting, c.UsageCleanupTask, c.UsageLog,
|
||||||
c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue,
|
c.User, c.UserAllowedGroup, c.UserAttributeDefinition, c.UserAttributeValue,
|
||||||
c.UserSubscription,
|
c.UserSubscription,
|
||||||
} {
|
} {
|
||||||
n.Intercept(interceptors...)
|
n.Intercept(interceptors...)
|
||||||
@@ -338,6 +344,8 @@ func (c *Client) Mutate(ctx context.Context, m Mutation) (Value, error) {
|
|||||||
return c.Proxy.mutate(ctx, m)
|
return c.Proxy.mutate(ctx, m)
|
||||||
case *RedeemCodeMutation:
|
case *RedeemCodeMutation:
|
||||||
return c.RedeemCode.mutate(ctx, m)
|
return c.RedeemCode.mutate(ctx, m)
|
||||||
|
case *SecuritySecretMutation:
|
||||||
|
return c.SecuritySecret.mutate(ctx, m)
|
||||||
case *SettingMutation:
|
case *SettingMutation:
|
||||||
return c.Setting.mutate(ctx, m)
|
return c.Setting.mutate(ctx, m)
|
||||||
case *UsageCleanupTaskMutation:
|
case *UsageCleanupTaskMutation:
|
||||||
@@ -2197,6 +2205,139 @@ func (c *RedeemCodeClient) mutate(ctx context.Context, m *RedeemCodeMutation) (V
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecuritySecretClient is a client for the SecuritySecret schema.
|
||||||
|
type SecuritySecretClient struct {
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSecuritySecretClient returns a client for the SecuritySecret from the given config.
|
||||||
|
func NewSecuritySecretClient(c config) *SecuritySecretClient {
|
||||||
|
return &SecuritySecretClient{config: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use adds a list of mutation hooks to the hooks stack.
|
||||||
|
// A call to `Use(f, g, h)` equals to `securitysecret.Hooks(f(g(h())))`.
|
||||||
|
func (c *SecuritySecretClient) Use(hooks ...Hook) {
|
||||||
|
c.hooks.SecuritySecret = append(c.hooks.SecuritySecret, hooks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intercept adds a list of query interceptors to the interceptors stack.
|
||||||
|
// A call to `Intercept(f, g, h)` equals to `securitysecret.Intercept(f(g(h())))`.
|
||||||
|
func (c *SecuritySecretClient) Intercept(interceptors ...Interceptor) {
|
||||||
|
c.inters.SecuritySecret = append(c.inters.SecuritySecret, interceptors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create returns a builder for creating a SecuritySecret entity.
|
||||||
|
func (c *SecuritySecretClient) Create() *SecuritySecretCreate {
|
||||||
|
mutation := newSecuritySecretMutation(c.config, OpCreate)
|
||||||
|
return &SecuritySecretCreate{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBulk returns a builder for creating a bulk of SecuritySecret entities.
|
||||||
|
func (c *SecuritySecretClient) CreateBulk(builders ...*SecuritySecretCreate) *SecuritySecretCreateBulk {
|
||||||
|
return &SecuritySecretCreateBulk{config: c.config, builders: builders}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapCreateBulk creates a bulk creation builder from the given slice. For each item in the slice, the function creates
|
||||||
|
// a builder and applies setFunc on it.
|
||||||
|
func (c *SecuritySecretClient) MapCreateBulk(slice any, setFunc func(*SecuritySecretCreate, int)) *SecuritySecretCreateBulk {
|
||||||
|
rv := reflect.ValueOf(slice)
|
||||||
|
if rv.Kind() != reflect.Slice {
|
||||||
|
return &SecuritySecretCreateBulk{err: fmt.Errorf("calling to SecuritySecretClient.MapCreateBulk with wrong type %T, need slice", slice)}
|
||||||
|
}
|
||||||
|
builders := make([]*SecuritySecretCreate, rv.Len())
|
||||||
|
for i := 0; i < rv.Len(); i++ {
|
||||||
|
builders[i] = c.Create()
|
||||||
|
setFunc(builders[i], i)
|
||||||
|
}
|
||||||
|
return &SecuritySecretCreateBulk{config: c.config, builders: builders}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update returns an update builder for SecuritySecret.
|
||||||
|
func (c *SecuritySecretClient) Update() *SecuritySecretUpdate {
|
||||||
|
mutation := newSecuritySecretMutation(c.config, OpUpdate)
|
||||||
|
return &SecuritySecretUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateOne returns an update builder for the given entity.
|
||||||
|
func (c *SecuritySecretClient) UpdateOne(_m *SecuritySecret) *SecuritySecretUpdateOne {
|
||||||
|
mutation := newSecuritySecretMutation(c.config, OpUpdateOne, withSecuritySecret(_m))
|
||||||
|
return &SecuritySecretUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateOneID returns an update builder for the given id.
|
||||||
|
func (c *SecuritySecretClient) UpdateOneID(id int64) *SecuritySecretUpdateOne {
|
||||||
|
mutation := newSecuritySecretMutation(c.config, OpUpdateOne, withSecuritySecretID(id))
|
||||||
|
return &SecuritySecretUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete returns a delete builder for SecuritySecret.
|
||||||
|
func (c *SecuritySecretClient) Delete() *SecuritySecretDelete {
|
||||||
|
mutation := newSecuritySecretMutation(c.config, OpDelete)
|
||||||
|
return &SecuritySecretDelete{config: c.config, hooks: c.Hooks(), mutation: mutation}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOne returns a builder for deleting the given entity.
|
||||||
|
func (c *SecuritySecretClient) DeleteOne(_m *SecuritySecret) *SecuritySecretDeleteOne {
|
||||||
|
return c.DeleteOneID(_m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOneID returns a builder for deleting the given entity by its id.
|
||||||
|
func (c *SecuritySecretClient) DeleteOneID(id int64) *SecuritySecretDeleteOne {
|
||||||
|
builder := c.Delete().Where(securitysecret.ID(id))
|
||||||
|
builder.mutation.id = &id
|
||||||
|
builder.mutation.op = OpDeleteOne
|
||||||
|
return &SecuritySecretDeleteOne{builder}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query returns a query builder for SecuritySecret.
|
||||||
|
func (c *SecuritySecretClient) Query() *SecuritySecretQuery {
|
||||||
|
return &SecuritySecretQuery{
|
||||||
|
config: c.config,
|
||||||
|
ctx: &QueryContext{Type: TypeSecuritySecret},
|
||||||
|
inters: c.Interceptors(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a SecuritySecret entity by its id.
|
||||||
|
func (c *SecuritySecretClient) Get(ctx context.Context, id int64) (*SecuritySecret, error) {
|
||||||
|
return c.Query().Where(securitysecret.ID(id)).Only(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetX is like Get, but panics if an error occurs.
|
||||||
|
func (c *SecuritySecretClient) GetX(ctx context.Context, id int64) *SecuritySecret {
|
||||||
|
obj, err := c.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hooks returns the client hooks.
|
||||||
|
func (c *SecuritySecretClient) Hooks() []Hook {
|
||||||
|
return c.hooks.SecuritySecret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interceptors returns the client interceptors.
|
||||||
|
func (c *SecuritySecretClient) Interceptors() []Interceptor {
|
||||||
|
return c.inters.SecuritySecret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SecuritySecretClient) mutate(ctx context.Context, m *SecuritySecretMutation) (Value, error) {
|
||||||
|
switch m.Op() {
|
||||||
|
case OpCreate:
|
||||||
|
return (&SecuritySecretCreate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
|
||||||
|
case OpUpdate:
|
||||||
|
return (&SecuritySecretUpdate{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
|
||||||
|
case OpUpdateOne:
|
||||||
|
return (&SecuritySecretUpdateOne{config: c.config, hooks: c.Hooks(), mutation: m}).Save(ctx)
|
||||||
|
case OpDelete, OpDeleteOne:
|
||||||
|
return (&SecuritySecretDelete{config: c.config, hooks: c.Hooks(), mutation: m}).Exec(ctx)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("ent: unknown SecuritySecret mutation op: %q", m.Op())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SettingClient is a client for the Setting schema.
|
// SettingClient is a client for the Setting schema.
|
||||||
type SettingClient struct {
|
type SettingClient struct {
|
||||||
config
|
config
|
||||||
@@ -3607,13 +3748,13 @@ type (
|
|||||||
hooks struct {
|
hooks struct {
|
||||||
APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
|
APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
|
||||||
ErrorPassthroughRule, Group, PromoCode, PromoCodeUsage, Proxy, RedeemCode,
|
ErrorPassthroughRule, Group, PromoCode, PromoCodeUsage, Proxy, RedeemCode,
|
||||||
Setting, UsageCleanupTask, UsageLog, User, UserAllowedGroup,
|
SecuritySecret, Setting, UsageCleanupTask, UsageLog, User, UserAllowedGroup,
|
||||||
UserAttributeDefinition, UserAttributeValue, UserSubscription []ent.Hook
|
UserAttributeDefinition, UserAttributeValue, UserSubscription []ent.Hook
|
||||||
}
|
}
|
||||||
inters struct {
|
inters struct {
|
||||||
APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
|
APIKey, Account, AccountGroup, Announcement, AnnouncementRead,
|
||||||
ErrorPassthroughRule, Group, PromoCode, PromoCodeUsage, Proxy, RedeemCode,
|
ErrorPassthroughRule, Group, PromoCode, PromoCodeUsage, Proxy, RedeemCode,
|
||||||
Setting, UsageCleanupTask, UsageLog, User, UserAllowedGroup,
|
SecuritySecret, Setting, UsageCleanupTask, UsageLog, User, UserAllowedGroup,
|
||||||
UserAttributeDefinition, UserAttributeValue, UserSubscription []ent.Interceptor
|
UserAttributeDefinition, UserAttributeValue, UserSubscription []ent.Interceptor
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
|
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/proxy"
|
"github.com/Wei-Shaw/sub2api/ent/proxy"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
"github.com/Wei-Shaw/sub2api/ent/setting"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||||
@@ -102,6 +103,7 @@ func checkColumn(t, c string) error {
|
|||||||
promocodeusage.Table: promocodeusage.ValidColumn,
|
promocodeusage.Table: promocodeusage.ValidColumn,
|
||||||
proxy.Table: proxy.ValidColumn,
|
proxy.Table: proxy.ValidColumn,
|
||||||
redeemcode.Table: redeemcode.ValidColumn,
|
redeemcode.Table: redeemcode.ValidColumn,
|
||||||
|
securitysecret.Table: securitysecret.ValidColumn,
|
||||||
setting.Table: setting.ValidColumn,
|
setting.Table: setting.ValidColumn,
|
||||||
usagecleanuptask.Table: usagecleanuptask.ValidColumn,
|
usagecleanuptask.Table: usagecleanuptask.ValidColumn,
|
||||||
usagelog.Table: usagelog.ValidColumn,
|
usagelog.Table: usagelog.ValidColumn,
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ type ErrorPassthroughRule struct {
|
|||||||
PassthroughBody bool `json:"passthrough_body,omitempty"`
|
PassthroughBody bool `json:"passthrough_body,omitempty"`
|
||||||
// CustomMessage holds the value of the "custom_message" field.
|
// CustomMessage holds the value of the "custom_message" field.
|
||||||
CustomMessage *string `json:"custom_message,omitempty"`
|
CustomMessage *string `json:"custom_message,omitempty"`
|
||||||
|
// SkipMonitoring holds the value of the "skip_monitoring" field.
|
||||||
|
SkipMonitoring bool `json:"skip_monitoring,omitempty"`
|
||||||
// Description holds the value of the "description" field.
|
// Description holds the value of the "description" field.
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
selectValues sql.SelectValues
|
selectValues sql.SelectValues
|
||||||
@@ -56,7 +58,7 @@ func (*ErrorPassthroughRule) scanValues(columns []string) ([]any, error) {
|
|||||||
switch columns[i] {
|
switch columns[i] {
|
||||||
case errorpassthroughrule.FieldErrorCodes, errorpassthroughrule.FieldKeywords, errorpassthroughrule.FieldPlatforms:
|
case errorpassthroughrule.FieldErrorCodes, errorpassthroughrule.FieldKeywords, errorpassthroughrule.FieldPlatforms:
|
||||||
values[i] = new([]byte)
|
values[i] = new([]byte)
|
||||||
case errorpassthroughrule.FieldEnabled, errorpassthroughrule.FieldPassthroughCode, errorpassthroughrule.FieldPassthroughBody:
|
case errorpassthroughrule.FieldEnabled, errorpassthroughrule.FieldPassthroughCode, errorpassthroughrule.FieldPassthroughBody, errorpassthroughrule.FieldSkipMonitoring:
|
||||||
values[i] = new(sql.NullBool)
|
values[i] = new(sql.NullBool)
|
||||||
case errorpassthroughrule.FieldID, errorpassthroughrule.FieldPriority, errorpassthroughrule.FieldResponseCode:
|
case errorpassthroughrule.FieldID, errorpassthroughrule.FieldPriority, errorpassthroughrule.FieldResponseCode:
|
||||||
values[i] = new(sql.NullInt64)
|
values[i] = new(sql.NullInt64)
|
||||||
@@ -171,6 +173,12 @@ func (_m *ErrorPassthroughRule) assignValues(columns []string, values []any) err
|
|||||||
_m.CustomMessage = new(string)
|
_m.CustomMessage = new(string)
|
||||||
*_m.CustomMessage = value.String
|
*_m.CustomMessage = value.String
|
||||||
}
|
}
|
||||||
|
case errorpassthroughrule.FieldSkipMonitoring:
|
||||||
|
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field skip_monitoring", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.SkipMonitoring = value.Bool
|
||||||
|
}
|
||||||
case errorpassthroughrule.FieldDescription:
|
case errorpassthroughrule.FieldDescription:
|
||||||
if value, ok := values[i].(*sql.NullString); !ok {
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field description", values[i])
|
return fmt.Errorf("unexpected type %T for field description", values[i])
|
||||||
@@ -257,6 +265,9 @@ func (_m *ErrorPassthroughRule) String() string {
|
|||||||
builder.WriteString(*v)
|
builder.WriteString(*v)
|
||||||
}
|
}
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
builder.WriteString("skip_monitoring=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", _m.SkipMonitoring))
|
||||||
|
builder.WriteString(", ")
|
||||||
if v := _m.Description; v != nil {
|
if v := _m.Description; v != nil {
|
||||||
builder.WriteString("description=")
|
builder.WriteString("description=")
|
||||||
builder.WriteString(*v)
|
builder.WriteString(*v)
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ const (
|
|||||||
FieldPassthroughBody = "passthrough_body"
|
FieldPassthroughBody = "passthrough_body"
|
||||||
// FieldCustomMessage holds the string denoting the custom_message field in the database.
|
// FieldCustomMessage holds the string denoting the custom_message field in the database.
|
||||||
FieldCustomMessage = "custom_message"
|
FieldCustomMessage = "custom_message"
|
||||||
|
// FieldSkipMonitoring holds the string denoting the skip_monitoring field in the database.
|
||||||
|
FieldSkipMonitoring = "skip_monitoring"
|
||||||
// FieldDescription holds the string denoting the description field in the database.
|
// FieldDescription holds the string denoting the description field in the database.
|
||||||
FieldDescription = "description"
|
FieldDescription = "description"
|
||||||
// Table holds the table name of the errorpassthroughrule in the database.
|
// Table holds the table name of the errorpassthroughrule in the database.
|
||||||
@@ -61,6 +63,7 @@ var Columns = []string{
|
|||||||
FieldResponseCode,
|
FieldResponseCode,
|
||||||
FieldPassthroughBody,
|
FieldPassthroughBody,
|
||||||
FieldCustomMessage,
|
FieldCustomMessage,
|
||||||
|
FieldSkipMonitoring,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +98,8 @@ var (
|
|||||||
DefaultPassthroughCode bool
|
DefaultPassthroughCode bool
|
||||||
// DefaultPassthroughBody holds the default value on creation for the "passthrough_body" field.
|
// DefaultPassthroughBody holds the default value on creation for the "passthrough_body" field.
|
||||||
DefaultPassthroughBody bool
|
DefaultPassthroughBody bool
|
||||||
|
// DefaultSkipMonitoring holds the default value on creation for the "skip_monitoring" field.
|
||||||
|
DefaultSkipMonitoring bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// OrderOption defines the ordering options for the ErrorPassthroughRule queries.
|
// OrderOption defines the ordering options for the ErrorPassthroughRule queries.
|
||||||
@@ -155,6 +160,11 @@ func ByCustomMessage(opts ...sql.OrderTermOption) OrderOption {
|
|||||||
return sql.OrderByField(FieldCustomMessage, opts...).ToFunc()
|
return sql.OrderByField(FieldCustomMessage, opts...).ToFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BySkipMonitoring orders the results by the skip_monitoring field.
|
||||||
|
func BySkipMonitoring(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldSkipMonitoring, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
// ByDescription orders the results by the description field.
|
// ByDescription orders the results by the description field.
|
||||||
func ByDescription(opts ...sql.OrderTermOption) OrderOption {
|
func ByDescription(opts ...sql.OrderTermOption) OrderOption {
|
||||||
return sql.OrderByField(FieldDescription, opts...).ToFunc()
|
return sql.OrderByField(FieldDescription, opts...).ToFunc()
|
||||||
|
|||||||
@@ -104,6 +104,11 @@ func CustomMessage(v string) predicate.ErrorPassthroughRule {
|
|||||||
return predicate.ErrorPassthroughRule(sql.FieldEQ(FieldCustomMessage, v))
|
return predicate.ErrorPassthroughRule(sql.FieldEQ(FieldCustomMessage, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SkipMonitoring applies equality check predicate on the "skip_monitoring" field. It's identical to SkipMonitoringEQ.
|
||||||
|
func SkipMonitoring(v bool) predicate.ErrorPassthroughRule {
|
||||||
|
return predicate.ErrorPassthroughRule(sql.FieldEQ(FieldSkipMonitoring, v))
|
||||||
|
}
|
||||||
|
|
||||||
// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ.
|
// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ.
|
||||||
func Description(v string) predicate.ErrorPassthroughRule {
|
func Description(v string) predicate.ErrorPassthroughRule {
|
||||||
return predicate.ErrorPassthroughRule(sql.FieldEQ(FieldDescription, v))
|
return predicate.ErrorPassthroughRule(sql.FieldEQ(FieldDescription, v))
|
||||||
@@ -544,6 +549,16 @@ func CustomMessageContainsFold(v string) predicate.ErrorPassthroughRule {
|
|||||||
return predicate.ErrorPassthroughRule(sql.FieldContainsFold(FieldCustomMessage, v))
|
return predicate.ErrorPassthroughRule(sql.FieldContainsFold(FieldCustomMessage, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SkipMonitoringEQ applies the EQ predicate on the "skip_monitoring" field.
|
||||||
|
func SkipMonitoringEQ(v bool) predicate.ErrorPassthroughRule {
|
||||||
|
return predicate.ErrorPassthroughRule(sql.FieldEQ(FieldSkipMonitoring, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipMonitoringNEQ applies the NEQ predicate on the "skip_monitoring" field.
|
||||||
|
func SkipMonitoringNEQ(v bool) predicate.ErrorPassthroughRule {
|
||||||
|
return predicate.ErrorPassthroughRule(sql.FieldNEQ(FieldSkipMonitoring, v))
|
||||||
|
}
|
||||||
|
|
||||||
// DescriptionEQ applies the EQ predicate on the "description" field.
|
// DescriptionEQ applies the EQ predicate on the "description" field.
|
||||||
func DescriptionEQ(v string) predicate.ErrorPassthroughRule {
|
func DescriptionEQ(v string) predicate.ErrorPassthroughRule {
|
||||||
return predicate.ErrorPassthroughRule(sql.FieldEQ(FieldDescription, v))
|
return predicate.ErrorPassthroughRule(sql.FieldEQ(FieldDescription, v))
|
||||||
|
|||||||
@@ -172,6 +172,20 @@ func (_c *ErrorPassthroughRuleCreate) SetNillableCustomMessage(v *string) *Error
|
|||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSkipMonitoring sets the "skip_monitoring" field.
|
||||||
|
func (_c *ErrorPassthroughRuleCreate) SetSkipMonitoring(v bool) *ErrorPassthroughRuleCreate {
|
||||||
|
_c.mutation.SetSkipMonitoring(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSkipMonitoring sets the "skip_monitoring" field if the given value is not nil.
|
||||||
|
func (_c *ErrorPassthroughRuleCreate) SetNillableSkipMonitoring(v *bool) *ErrorPassthroughRuleCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetSkipMonitoring(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SetDescription sets the "description" field.
|
// SetDescription sets the "description" field.
|
||||||
func (_c *ErrorPassthroughRuleCreate) SetDescription(v string) *ErrorPassthroughRuleCreate {
|
func (_c *ErrorPassthroughRuleCreate) SetDescription(v string) *ErrorPassthroughRuleCreate {
|
||||||
_c.mutation.SetDescription(v)
|
_c.mutation.SetDescription(v)
|
||||||
@@ -249,6 +263,10 @@ func (_c *ErrorPassthroughRuleCreate) defaults() {
|
|||||||
v := errorpassthroughrule.DefaultPassthroughBody
|
v := errorpassthroughrule.DefaultPassthroughBody
|
||||||
_c.mutation.SetPassthroughBody(v)
|
_c.mutation.SetPassthroughBody(v)
|
||||||
}
|
}
|
||||||
|
if _, ok := _c.mutation.SkipMonitoring(); !ok {
|
||||||
|
v := errorpassthroughrule.DefaultSkipMonitoring
|
||||||
|
_c.mutation.SetSkipMonitoring(v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check runs all checks and user-defined validators on the builder.
|
// check runs all checks and user-defined validators on the builder.
|
||||||
@@ -287,6 +305,9 @@ func (_c *ErrorPassthroughRuleCreate) check() error {
|
|||||||
if _, ok := _c.mutation.PassthroughBody(); !ok {
|
if _, ok := _c.mutation.PassthroughBody(); !ok {
|
||||||
return &ValidationError{Name: "passthrough_body", err: errors.New(`ent: missing required field "ErrorPassthroughRule.passthrough_body"`)}
|
return &ValidationError{Name: "passthrough_body", err: errors.New(`ent: missing required field "ErrorPassthroughRule.passthrough_body"`)}
|
||||||
}
|
}
|
||||||
|
if _, ok := _c.mutation.SkipMonitoring(); !ok {
|
||||||
|
return &ValidationError{Name: "skip_monitoring", err: errors.New(`ent: missing required field "ErrorPassthroughRule.skip_monitoring"`)}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +387,10 @@ func (_c *ErrorPassthroughRuleCreate) createSpec() (*ErrorPassthroughRule, *sqlg
|
|||||||
_spec.SetField(errorpassthroughrule.FieldCustomMessage, field.TypeString, value)
|
_spec.SetField(errorpassthroughrule.FieldCustomMessage, field.TypeString, value)
|
||||||
_node.CustomMessage = &value
|
_node.CustomMessage = &value
|
||||||
}
|
}
|
||||||
|
if value, ok := _c.mutation.SkipMonitoring(); ok {
|
||||||
|
_spec.SetField(errorpassthroughrule.FieldSkipMonitoring, field.TypeBool, value)
|
||||||
|
_node.SkipMonitoring = value
|
||||||
|
}
|
||||||
if value, ok := _c.mutation.Description(); ok {
|
if value, ok := _c.mutation.Description(); ok {
|
||||||
_spec.SetField(errorpassthroughrule.FieldDescription, field.TypeString, value)
|
_spec.SetField(errorpassthroughrule.FieldDescription, field.TypeString, value)
|
||||||
_node.Description = &value
|
_node.Description = &value
|
||||||
@@ -608,6 +633,18 @@ func (u *ErrorPassthroughRuleUpsert) ClearCustomMessage() *ErrorPassthroughRuleU
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSkipMonitoring sets the "skip_monitoring" field.
|
||||||
|
func (u *ErrorPassthroughRuleUpsert) SetSkipMonitoring(v bool) *ErrorPassthroughRuleUpsert {
|
||||||
|
u.Set(errorpassthroughrule.FieldSkipMonitoring, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSkipMonitoring sets the "skip_monitoring" field to the value that was provided on create.
|
||||||
|
func (u *ErrorPassthroughRuleUpsert) UpdateSkipMonitoring() *ErrorPassthroughRuleUpsert {
|
||||||
|
u.SetExcluded(errorpassthroughrule.FieldSkipMonitoring)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
// SetDescription sets the "description" field.
|
// SetDescription sets the "description" field.
|
||||||
func (u *ErrorPassthroughRuleUpsert) SetDescription(v string) *ErrorPassthroughRuleUpsert {
|
func (u *ErrorPassthroughRuleUpsert) SetDescription(v string) *ErrorPassthroughRuleUpsert {
|
||||||
u.Set(errorpassthroughrule.FieldDescription, v)
|
u.Set(errorpassthroughrule.FieldDescription, v)
|
||||||
@@ -888,6 +925,20 @@ func (u *ErrorPassthroughRuleUpsertOne) ClearCustomMessage() *ErrorPassthroughRu
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSkipMonitoring sets the "skip_monitoring" field.
|
||||||
|
func (u *ErrorPassthroughRuleUpsertOne) SetSkipMonitoring(v bool) *ErrorPassthroughRuleUpsertOne {
|
||||||
|
return u.Update(func(s *ErrorPassthroughRuleUpsert) {
|
||||||
|
s.SetSkipMonitoring(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSkipMonitoring sets the "skip_monitoring" field to the value that was provided on create.
|
||||||
|
func (u *ErrorPassthroughRuleUpsertOne) UpdateSkipMonitoring() *ErrorPassthroughRuleUpsertOne {
|
||||||
|
return u.Update(func(s *ErrorPassthroughRuleUpsert) {
|
||||||
|
s.UpdateSkipMonitoring()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetDescription sets the "description" field.
|
// SetDescription sets the "description" field.
|
||||||
func (u *ErrorPassthroughRuleUpsertOne) SetDescription(v string) *ErrorPassthroughRuleUpsertOne {
|
func (u *ErrorPassthroughRuleUpsertOne) SetDescription(v string) *ErrorPassthroughRuleUpsertOne {
|
||||||
return u.Update(func(s *ErrorPassthroughRuleUpsert) {
|
return u.Update(func(s *ErrorPassthroughRuleUpsert) {
|
||||||
@@ -1337,6 +1388,20 @@ func (u *ErrorPassthroughRuleUpsertBulk) ClearCustomMessage() *ErrorPassthroughR
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSkipMonitoring sets the "skip_monitoring" field.
|
||||||
|
func (u *ErrorPassthroughRuleUpsertBulk) SetSkipMonitoring(v bool) *ErrorPassthroughRuleUpsertBulk {
|
||||||
|
return u.Update(func(s *ErrorPassthroughRuleUpsert) {
|
||||||
|
s.SetSkipMonitoring(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSkipMonitoring sets the "skip_monitoring" field to the value that was provided on create.
|
||||||
|
func (u *ErrorPassthroughRuleUpsertBulk) UpdateSkipMonitoring() *ErrorPassthroughRuleUpsertBulk {
|
||||||
|
return u.Update(func(s *ErrorPassthroughRuleUpsert) {
|
||||||
|
s.UpdateSkipMonitoring()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetDescription sets the "description" field.
|
// SetDescription sets the "description" field.
|
||||||
func (u *ErrorPassthroughRuleUpsertBulk) SetDescription(v string) *ErrorPassthroughRuleUpsertBulk {
|
func (u *ErrorPassthroughRuleUpsertBulk) SetDescription(v string) *ErrorPassthroughRuleUpsertBulk {
|
||||||
return u.Update(func(s *ErrorPassthroughRuleUpsert) {
|
return u.Update(func(s *ErrorPassthroughRuleUpsert) {
|
||||||
|
|||||||
@@ -227,6 +227,20 @@ func (_u *ErrorPassthroughRuleUpdate) ClearCustomMessage() *ErrorPassthroughRule
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSkipMonitoring sets the "skip_monitoring" field.
|
||||||
|
func (_u *ErrorPassthroughRuleUpdate) SetSkipMonitoring(v bool) *ErrorPassthroughRuleUpdate {
|
||||||
|
_u.mutation.SetSkipMonitoring(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSkipMonitoring sets the "skip_monitoring" field if the given value is not nil.
|
||||||
|
func (_u *ErrorPassthroughRuleUpdate) SetNillableSkipMonitoring(v *bool) *ErrorPassthroughRuleUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetSkipMonitoring(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetDescription sets the "description" field.
|
// SetDescription sets the "description" field.
|
||||||
func (_u *ErrorPassthroughRuleUpdate) SetDescription(v string) *ErrorPassthroughRuleUpdate {
|
func (_u *ErrorPassthroughRuleUpdate) SetDescription(v string) *ErrorPassthroughRuleUpdate {
|
||||||
_u.mutation.SetDescription(v)
|
_u.mutation.SetDescription(v)
|
||||||
@@ -387,6 +401,9 @@ func (_u *ErrorPassthroughRuleUpdate) sqlSave(ctx context.Context) (_node int, e
|
|||||||
if _u.mutation.CustomMessageCleared() {
|
if _u.mutation.CustomMessageCleared() {
|
||||||
_spec.ClearField(errorpassthroughrule.FieldCustomMessage, field.TypeString)
|
_spec.ClearField(errorpassthroughrule.FieldCustomMessage, field.TypeString)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.SkipMonitoring(); ok {
|
||||||
|
_spec.SetField(errorpassthroughrule.FieldSkipMonitoring, field.TypeBool, value)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.Description(); ok {
|
if value, ok := _u.mutation.Description(); ok {
|
||||||
_spec.SetField(errorpassthroughrule.FieldDescription, field.TypeString, value)
|
_spec.SetField(errorpassthroughrule.FieldDescription, field.TypeString, value)
|
||||||
}
|
}
|
||||||
@@ -611,6 +628,20 @@ func (_u *ErrorPassthroughRuleUpdateOne) ClearCustomMessage() *ErrorPassthroughR
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSkipMonitoring sets the "skip_monitoring" field.
|
||||||
|
func (_u *ErrorPassthroughRuleUpdateOne) SetSkipMonitoring(v bool) *ErrorPassthroughRuleUpdateOne {
|
||||||
|
_u.mutation.SetSkipMonitoring(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSkipMonitoring sets the "skip_monitoring" field if the given value is not nil.
|
||||||
|
func (_u *ErrorPassthroughRuleUpdateOne) SetNillableSkipMonitoring(v *bool) *ErrorPassthroughRuleUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetSkipMonitoring(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetDescription sets the "description" field.
|
// SetDescription sets the "description" field.
|
||||||
func (_u *ErrorPassthroughRuleUpdateOne) SetDescription(v string) *ErrorPassthroughRuleUpdateOne {
|
func (_u *ErrorPassthroughRuleUpdateOne) SetDescription(v string) *ErrorPassthroughRuleUpdateOne {
|
||||||
_u.mutation.SetDescription(v)
|
_u.mutation.SetDescription(v)
|
||||||
@@ -801,6 +832,9 @@ func (_u *ErrorPassthroughRuleUpdateOne) sqlSave(ctx context.Context) (_node *Er
|
|||||||
if _u.mutation.CustomMessageCleared() {
|
if _u.mutation.CustomMessageCleared() {
|
||||||
_spec.ClearField(errorpassthroughrule.FieldCustomMessage, field.TypeString)
|
_spec.ClearField(errorpassthroughrule.FieldCustomMessage, field.TypeString)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.SkipMonitoring(); ok {
|
||||||
|
_spec.SetField(errorpassthroughrule.FieldSkipMonitoring, field.TypeBool, value)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.Description(); ok {
|
if value, ok := _u.mutation.Description(); ok {
|
||||||
_spec.SetField(errorpassthroughrule.FieldDescription, field.TypeString, value)
|
_spec.SetField(errorpassthroughrule.FieldDescription, field.TypeString, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ type Group struct {
|
|||||||
ImagePrice2k *float64 `json:"image_price_2k,omitempty"`
|
ImagePrice2k *float64 `json:"image_price_2k,omitempty"`
|
||||||
// ImagePrice4k holds the value of the "image_price_4k" field.
|
// ImagePrice4k holds the value of the "image_price_4k" field.
|
||||||
ImagePrice4k *float64 `json:"image_price_4k,omitempty"`
|
ImagePrice4k *float64 `json:"image_price_4k,omitempty"`
|
||||||
|
// SoraImagePrice360 holds the value of the "sora_image_price_360" field.
|
||||||
|
SoraImagePrice360 *float64 `json:"sora_image_price_360,omitempty"`
|
||||||
|
// SoraImagePrice540 holds the value of the "sora_image_price_540" field.
|
||||||
|
SoraImagePrice540 *float64 `json:"sora_image_price_540,omitempty"`
|
||||||
|
// SoraVideoPricePerRequest holds the value of the "sora_video_price_per_request" field.
|
||||||
|
SoraVideoPricePerRequest *float64 `json:"sora_video_price_per_request,omitempty"`
|
||||||
|
// SoraVideoPricePerRequestHd holds the value of the "sora_video_price_per_request_hd" field.
|
||||||
|
SoraVideoPricePerRequestHd *float64 `json:"sora_video_price_per_request_hd,omitempty"`
|
||||||
// 是否仅允许 Claude Code 客户端
|
// 是否仅允许 Claude Code 客户端
|
||||||
ClaudeCodeOnly bool `json:"claude_code_only,omitempty"`
|
ClaudeCodeOnly bool `json:"claude_code_only,omitempty"`
|
||||||
// 非 Claude Code 请求降级使用的分组 ID
|
// 非 Claude Code 请求降级使用的分组 ID
|
||||||
@@ -178,7 +186,7 @@ func (*Group) scanValues(columns []string) ([]any, error) {
|
|||||||
values[i] = new([]byte)
|
values[i] = new([]byte)
|
||||||
case group.FieldIsExclusive, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject:
|
case group.FieldIsExclusive, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject:
|
||||||
values[i] = new(sql.NullBool)
|
values[i] = new(sql.NullBool)
|
||||||
case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k:
|
case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k, group.FieldSoraImagePrice360, group.FieldSoraImagePrice540, group.FieldSoraVideoPricePerRequest, group.FieldSoraVideoPricePerRequestHd:
|
||||||
values[i] = new(sql.NullFloat64)
|
values[i] = new(sql.NullFloat64)
|
||||||
case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder:
|
case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder:
|
||||||
values[i] = new(sql.NullInt64)
|
values[i] = new(sql.NullInt64)
|
||||||
@@ -317,6 +325,34 @@ func (_m *Group) assignValues(columns []string, values []any) error {
|
|||||||
_m.ImagePrice4k = new(float64)
|
_m.ImagePrice4k = new(float64)
|
||||||
*_m.ImagePrice4k = value.Float64
|
*_m.ImagePrice4k = value.Float64
|
||||||
}
|
}
|
||||||
|
case group.FieldSoraImagePrice360:
|
||||||
|
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field sora_image_price_360", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.SoraImagePrice360 = new(float64)
|
||||||
|
*_m.SoraImagePrice360 = value.Float64
|
||||||
|
}
|
||||||
|
case group.FieldSoraImagePrice540:
|
||||||
|
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field sora_image_price_540", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.SoraImagePrice540 = new(float64)
|
||||||
|
*_m.SoraImagePrice540 = value.Float64
|
||||||
|
}
|
||||||
|
case group.FieldSoraVideoPricePerRequest:
|
||||||
|
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field sora_video_price_per_request", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.SoraVideoPricePerRequest = new(float64)
|
||||||
|
*_m.SoraVideoPricePerRequest = value.Float64
|
||||||
|
}
|
||||||
|
case group.FieldSoraVideoPricePerRequestHd:
|
||||||
|
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field sora_video_price_per_request_hd", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.SoraVideoPricePerRequestHd = new(float64)
|
||||||
|
*_m.SoraVideoPricePerRequestHd = value.Float64
|
||||||
|
}
|
||||||
case group.FieldClaudeCodeOnly:
|
case group.FieldClaudeCodeOnly:
|
||||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field claude_code_only", values[i])
|
return fmt.Errorf("unexpected type %T for field claude_code_only", values[i])
|
||||||
@@ -514,6 +550,26 @@ func (_m *Group) String() string {
|
|||||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||||
}
|
}
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
if v := _m.SoraImagePrice360; v != nil {
|
||||||
|
builder.WriteString("sora_image_price_360=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||||
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
|
if v := _m.SoraImagePrice540; v != nil {
|
||||||
|
builder.WriteString("sora_image_price_540=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||||
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
|
if v := _m.SoraVideoPricePerRequest; v != nil {
|
||||||
|
builder.WriteString("sora_video_price_per_request=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||||
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
|
if v := _m.SoraVideoPricePerRequestHd; v != nil {
|
||||||
|
builder.WriteString("sora_video_price_per_request_hd=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||||
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
builder.WriteString("claude_code_only=")
|
builder.WriteString("claude_code_only=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", _m.ClaudeCodeOnly))
|
builder.WriteString(fmt.Sprintf("%v", _m.ClaudeCodeOnly))
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ const (
|
|||||||
FieldImagePrice2k = "image_price_2k"
|
FieldImagePrice2k = "image_price_2k"
|
||||||
// FieldImagePrice4k holds the string denoting the image_price_4k field in the database.
|
// FieldImagePrice4k holds the string denoting the image_price_4k field in the database.
|
||||||
FieldImagePrice4k = "image_price_4k"
|
FieldImagePrice4k = "image_price_4k"
|
||||||
|
// FieldSoraImagePrice360 holds the string denoting the sora_image_price_360 field in the database.
|
||||||
|
FieldSoraImagePrice360 = "sora_image_price_360"
|
||||||
|
// FieldSoraImagePrice540 holds the string denoting the sora_image_price_540 field in the database.
|
||||||
|
FieldSoraImagePrice540 = "sora_image_price_540"
|
||||||
|
// FieldSoraVideoPricePerRequest holds the string denoting the sora_video_price_per_request field in the database.
|
||||||
|
FieldSoraVideoPricePerRequest = "sora_video_price_per_request"
|
||||||
|
// FieldSoraVideoPricePerRequestHd holds the string denoting the sora_video_price_per_request_hd field in the database.
|
||||||
|
FieldSoraVideoPricePerRequestHd = "sora_video_price_per_request_hd"
|
||||||
// FieldClaudeCodeOnly holds the string denoting the claude_code_only field in the database.
|
// FieldClaudeCodeOnly holds the string denoting the claude_code_only field in the database.
|
||||||
FieldClaudeCodeOnly = "claude_code_only"
|
FieldClaudeCodeOnly = "claude_code_only"
|
||||||
// FieldFallbackGroupID holds the string denoting the fallback_group_id field in the database.
|
// FieldFallbackGroupID holds the string denoting the fallback_group_id field in the database.
|
||||||
@@ -157,6 +165,10 @@ var Columns = []string{
|
|||||||
FieldImagePrice1k,
|
FieldImagePrice1k,
|
||||||
FieldImagePrice2k,
|
FieldImagePrice2k,
|
||||||
FieldImagePrice4k,
|
FieldImagePrice4k,
|
||||||
|
FieldSoraImagePrice360,
|
||||||
|
FieldSoraImagePrice540,
|
||||||
|
FieldSoraVideoPricePerRequest,
|
||||||
|
FieldSoraVideoPricePerRequestHd,
|
||||||
FieldClaudeCodeOnly,
|
FieldClaudeCodeOnly,
|
||||||
FieldFallbackGroupID,
|
FieldFallbackGroupID,
|
||||||
FieldFallbackGroupIDOnInvalidRequest,
|
FieldFallbackGroupIDOnInvalidRequest,
|
||||||
@@ -325,6 +337,26 @@ func ByImagePrice4k(opts ...sql.OrderTermOption) OrderOption {
|
|||||||
return sql.OrderByField(FieldImagePrice4k, opts...).ToFunc()
|
return sql.OrderByField(FieldImagePrice4k, opts...).ToFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BySoraImagePrice360 orders the results by the sora_image_price_360 field.
|
||||||
|
func BySoraImagePrice360(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldSoraImagePrice360, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BySoraImagePrice540 orders the results by the sora_image_price_540 field.
|
||||||
|
func BySoraImagePrice540(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldSoraImagePrice540, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BySoraVideoPricePerRequest orders the results by the sora_video_price_per_request field.
|
||||||
|
func BySoraVideoPricePerRequest(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldSoraVideoPricePerRequest, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BySoraVideoPricePerRequestHd orders the results by the sora_video_price_per_request_hd field.
|
||||||
|
func BySoraVideoPricePerRequestHd(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldSoraVideoPricePerRequestHd, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
// ByClaudeCodeOnly orders the results by the claude_code_only field.
|
// ByClaudeCodeOnly orders the results by the claude_code_only field.
|
||||||
func ByClaudeCodeOnly(opts ...sql.OrderTermOption) OrderOption {
|
func ByClaudeCodeOnly(opts ...sql.OrderTermOption) OrderOption {
|
||||||
return sql.OrderByField(FieldClaudeCodeOnly, opts...).ToFunc()
|
return sql.OrderByField(FieldClaudeCodeOnly, opts...).ToFunc()
|
||||||
|
|||||||
@@ -140,6 +140,26 @@ func ImagePrice4k(v float64) predicate.Group {
|
|||||||
return predicate.Group(sql.FieldEQ(FieldImagePrice4k, v))
|
return predicate.Group(sql.FieldEQ(FieldImagePrice4k, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360 applies equality check predicate on the "sora_image_price_360" field. It's identical to SoraImagePrice360EQ.
|
||||||
|
func SoraImagePrice360(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldEQ(FieldSoraImagePrice360, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540 applies equality check predicate on the "sora_image_price_540" field. It's identical to SoraImagePrice540EQ.
|
||||||
|
func SoraImagePrice540(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldEQ(FieldSoraImagePrice540, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequest applies equality check predicate on the "sora_video_price_per_request" field. It's identical to SoraVideoPricePerRequestEQ.
|
||||||
|
func SoraVideoPricePerRequest(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldEQ(FieldSoraVideoPricePerRequest, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHd applies equality check predicate on the "sora_video_price_per_request_hd" field. It's identical to SoraVideoPricePerRequestHdEQ.
|
||||||
|
func SoraVideoPricePerRequestHd(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldEQ(FieldSoraVideoPricePerRequestHd, v))
|
||||||
|
}
|
||||||
|
|
||||||
// ClaudeCodeOnly applies equality check predicate on the "claude_code_only" field. It's identical to ClaudeCodeOnlyEQ.
|
// ClaudeCodeOnly applies equality check predicate on the "claude_code_only" field. It's identical to ClaudeCodeOnlyEQ.
|
||||||
func ClaudeCodeOnly(v bool) predicate.Group {
|
func ClaudeCodeOnly(v bool) predicate.Group {
|
||||||
return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v))
|
return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v))
|
||||||
@@ -1025,6 +1045,206 @@ func ImagePrice4kNotNil() predicate.Group {
|
|||||||
return predicate.Group(sql.FieldNotNull(FieldImagePrice4k))
|
return predicate.Group(sql.FieldNotNull(FieldImagePrice4k))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360EQ applies the EQ predicate on the "sora_image_price_360" field.
|
||||||
|
func SoraImagePrice360EQ(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldEQ(FieldSoraImagePrice360, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360NEQ applies the NEQ predicate on the "sora_image_price_360" field.
|
||||||
|
func SoraImagePrice360NEQ(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNEQ(FieldSoraImagePrice360, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360In applies the In predicate on the "sora_image_price_360" field.
|
||||||
|
func SoraImagePrice360In(vs ...float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldIn(FieldSoraImagePrice360, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360NotIn applies the NotIn predicate on the "sora_image_price_360" field.
|
||||||
|
func SoraImagePrice360NotIn(vs ...float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNotIn(FieldSoraImagePrice360, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360GT applies the GT predicate on the "sora_image_price_360" field.
|
||||||
|
func SoraImagePrice360GT(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldGT(FieldSoraImagePrice360, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360GTE applies the GTE predicate on the "sora_image_price_360" field.
|
||||||
|
func SoraImagePrice360GTE(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldGTE(FieldSoraImagePrice360, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360LT applies the LT predicate on the "sora_image_price_360" field.
|
||||||
|
func SoraImagePrice360LT(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldLT(FieldSoraImagePrice360, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360LTE applies the LTE predicate on the "sora_image_price_360" field.
|
||||||
|
func SoraImagePrice360LTE(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldLTE(FieldSoraImagePrice360, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360IsNil applies the IsNil predicate on the "sora_image_price_360" field.
|
||||||
|
func SoraImagePrice360IsNil() predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldIsNull(FieldSoraImagePrice360))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice360NotNil applies the NotNil predicate on the "sora_image_price_360" field.
|
||||||
|
func SoraImagePrice360NotNil() predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNotNull(FieldSoraImagePrice360))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540EQ applies the EQ predicate on the "sora_image_price_540" field.
|
||||||
|
func SoraImagePrice540EQ(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldEQ(FieldSoraImagePrice540, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540NEQ applies the NEQ predicate on the "sora_image_price_540" field.
|
||||||
|
func SoraImagePrice540NEQ(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNEQ(FieldSoraImagePrice540, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540In applies the In predicate on the "sora_image_price_540" field.
|
||||||
|
func SoraImagePrice540In(vs ...float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldIn(FieldSoraImagePrice540, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540NotIn applies the NotIn predicate on the "sora_image_price_540" field.
|
||||||
|
func SoraImagePrice540NotIn(vs ...float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNotIn(FieldSoraImagePrice540, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540GT applies the GT predicate on the "sora_image_price_540" field.
|
||||||
|
func SoraImagePrice540GT(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldGT(FieldSoraImagePrice540, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540GTE applies the GTE predicate on the "sora_image_price_540" field.
|
||||||
|
func SoraImagePrice540GTE(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldGTE(FieldSoraImagePrice540, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540LT applies the LT predicate on the "sora_image_price_540" field.
|
||||||
|
func SoraImagePrice540LT(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldLT(FieldSoraImagePrice540, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540LTE applies the LTE predicate on the "sora_image_price_540" field.
|
||||||
|
func SoraImagePrice540LTE(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldLTE(FieldSoraImagePrice540, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540IsNil applies the IsNil predicate on the "sora_image_price_540" field.
|
||||||
|
func SoraImagePrice540IsNil() predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldIsNull(FieldSoraImagePrice540))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraImagePrice540NotNil applies the NotNil predicate on the "sora_image_price_540" field.
|
||||||
|
func SoraImagePrice540NotNil() predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNotNull(FieldSoraImagePrice540))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestEQ applies the EQ predicate on the "sora_video_price_per_request" field.
|
||||||
|
func SoraVideoPricePerRequestEQ(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldEQ(FieldSoraVideoPricePerRequest, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestNEQ applies the NEQ predicate on the "sora_video_price_per_request" field.
|
||||||
|
func SoraVideoPricePerRequestNEQ(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNEQ(FieldSoraVideoPricePerRequest, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestIn applies the In predicate on the "sora_video_price_per_request" field.
|
||||||
|
func SoraVideoPricePerRequestIn(vs ...float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldIn(FieldSoraVideoPricePerRequest, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestNotIn applies the NotIn predicate on the "sora_video_price_per_request" field.
|
||||||
|
func SoraVideoPricePerRequestNotIn(vs ...float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNotIn(FieldSoraVideoPricePerRequest, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestGT applies the GT predicate on the "sora_video_price_per_request" field.
|
||||||
|
func SoraVideoPricePerRequestGT(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldGT(FieldSoraVideoPricePerRequest, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestGTE applies the GTE predicate on the "sora_video_price_per_request" field.
|
||||||
|
func SoraVideoPricePerRequestGTE(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldGTE(FieldSoraVideoPricePerRequest, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestLT applies the LT predicate on the "sora_video_price_per_request" field.
|
||||||
|
func SoraVideoPricePerRequestLT(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldLT(FieldSoraVideoPricePerRequest, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestLTE applies the LTE predicate on the "sora_video_price_per_request" field.
|
||||||
|
func SoraVideoPricePerRequestLTE(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldLTE(FieldSoraVideoPricePerRequest, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestIsNil applies the IsNil predicate on the "sora_video_price_per_request" field.
|
||||||
|
func SoraVideoPricePerRequestIsNil() predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldIsNull(FieldSoraVideoPricePerRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestNotNil applies the NotNil predicate on the "sora_video_price_per_request" field.
|
||||||
|
func SoraVideoPricePerRequestNotNil() predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNotNull(FieldSoraVideoPricePerRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHdEQ applies the EQ predicate on the "sora_video_price_per_request_hd" field.
|
||||||
|
func SoraVideoPricePerRequestHdEQ(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldEQ(FieldSoraVideoPricePerRequestHd, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHdNEQ applies the NEQ predicate on the "sora_video_price_per_request_hd" field.
|
||||||
|
func SoraVideoPricePerRequestHdNEQ(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNEQ(FieldSoraVideoPricePerRequestHd, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHdIn applies the In predicate on the "sora_video_price_per_request_hd" field.
|
||||||
|
func SoraVideoPricePerRequestHdIn(vs ...float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldIn(FieldSoraVideoPricePerRequestHd, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHdNotIn applies the NotIn predicate on the "sora_video_price_per_request_hd" field.
|
||||||
|
func SoraVideoPricePerRequestHdNotIn(vs ...float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNotIn(FieldSoraVideoPricePerRequestHd, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHdGT applies the GT predicate on the "sora_video_price_per_request_hd" field.
|
||||||
|
func SoraVideoPricePerRequestHdGT(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldGT(FieldSoraVideoPricePerRequestHd, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHdGTE applies the GTE predicate on the "sora_video_price_per_request_hd" field.
|
||||||
|
func SoraVideoPricePerRequestHdGTE(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldGTE(FieldSoraVideoPricePerRequestHd, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHdLT applies the LT predicate on the "sora_video_price_per_request_hd" field.
|
||||||
|
func SoraVideoPricePerRequestHdLT(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldLT(FieldSoraVideoPricePerRequestHd, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHdLTE applies the LTE predicate on the "sora_video_price_per_request_hd" field.
|
||||||
|
func SoraVideoPricePerRequestHdLTE(v float64) predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldLTE(FieldSoraVideoPricePerRequestHd, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHdIsNil applies the IsNil predicate on the "sora_video_price_per_request_hd" field.
|
||||||
|
func SoraVideoPricePerRequestHdIsNil() predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldIsNull(FieldSoraVideoPricePerRequestHd))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraVideoPricePerRequestHdNotNil applies the NotNil predicate on the "sora_video_price_per_request_hd" field.
|
||||||
|
func SoraVideoPricePerRequestHdNotNil() predicate.Group {
|
||||||
|
return predicate.Group(sql.FieldNotNull(FieldSoraVideoPricePerRequestHd))
|
||||||
|
}
|
||||||
|
|
||||||
// ClaudeCodeOnlyEQ applies the EQ predicate on the "claude_code_only" field.
|
// ClaudeCodeOnlyEQ applies the EQ predicate on the "claude_code_only" field.
|
||||||
func ClaudeCodeOnlyEQ(v bool) predicate.Group {
|
func ClaudeCodeOnlyEQ(v bool) predicate.Group {
|
||||||
return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v))
|
return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v))
|
||||||
|
|||||||
@@ -258,6 +258,62 @@ func (_c *GroupCreate) SetNillableImagePrice4k(v *float64) *GroupCreate {
|
|||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice360 sets the "sora_image_price_360" field.
|
||||||
|
func (_c *GroupCreate) SetSoraImagePrice360(v float64) *GroupCreate {
|
||||||
|
_c.mutation.SetSoraImagePrice360(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraImagePrice360 sets the "sora_image_price_360" field if the given value is not nil.
|
||||||
|
func (_c *GroupCreate) SetNillableSoraImagePrice360(v *float64) *GroupCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetSoraImagePrice360(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice540 sets the "sora_image_price_540" field.
|
||||||
|
func (_c *GroupCreate) SetSoraImagePrice540(v float64) *GroupCreate {
|
||||||
|
_c.mutation.SetSoraImagePrice540(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraImagePrice540 sets the "sora_image_price_540" field if the given value is not nil.
|
||||||
|
func (_c *GroupCreate) SetNillableSoraImagePrice540(v *float64) *GroupCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetSoraImagePrice540(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequest sets the "sora_video_price_per_request" field.
|
||||||
|
func (_c *GroupCreate) SetSoraVideoPricePerRequest(v float64) *GroupCreate {
|
||||||
|
_c.mutation.SetSoraVideoPricePerRequest(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraVideoPricePerRequest sets the "sora_video_price_per_request" field if the given value is not nil.
|
||||||
|
func (_c *GroupCreate) SetNillableSoraVideoPricePerRequest(v *float64) *GroupCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetSoraVideoPricePerRequest(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field.
|
||||||
|
func (_c *GroupCreate) SetSoraVideoPricePerRequestHd(v float64) *GroupCreate {
|
||||||
|
_c.mutation.SetSoraVideoPricePerRequestHd(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field if the given value is not nil.
|
||||||
|
func (_c *GroupCreate) SetNillableSoraVideoPricePerRequestHd(v *float64) *GroupCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetSoraVideoPricePerRequestHd(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||||
func (_c *GroupCreate) SetClaudeCodeOnly(v bool) *GroupCreate {
|
func (_c *GroupCreate) SetClaudeCodeOnly(v bool) *GroupCreate {
|
||||||
_c.mutation.SetClaudeCodeOnly(v)
|
_c.mutation.SetClaudeCodeOnly(v)
|
||||||
@@ -701,6 +757,22 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
|
|||||||
_spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value)
|
_spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value)
|
||||||
_node.ImagePrice4k = &value
|
_node.ImagePrice4k = &value
|
||||||
}
|
}
|
||||||
|
if value, ok := _c.mutation.SoraImagePrice360(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraImagePrice360, field.TypeFloat64, value)
|
||||||
|
_node.SoraImagePrice360 = &value
|
||||||
|
}
|
||||||
|
if value, ok := _c.mutation.SoraImagePrice540(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraImagePrice540, field.TypeFloat64, value)
|
||||||
|
_node.SoraImagePrice540 = &value
|
||||||
|
}
|
||||||
|
if value, ok := _c.mutation.SoraVideoPricePerRequest(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraVideoPricePerRequest, field.TypeFloat64, value)
|
||||||
|
_node.SoraVideoPricePerRequest = &value
|
||||||
|
}
|
||||||
|
if value, ok := _c.mutation.SoraVideoPricePerRequestHd(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraVideoPricePerRequestHd, field.TypeFloat64, value)
|
||||||
|
_node.SoraVideoPricePerRequestHd = &value
|
||||||
|
}
|
||||||
if value, ok := _c.mutation.ClaudeCodeOnly(); ok {
|
if value, ok := _c.mutation.ClaudeCodeOnly(); ok {
|
||||||
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
|
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
|
||||||
_node.ClaudeCodeOnly = value
|
_node.ClaudeCodeOnly = value
|
||||||
@@ -1177,6 +1249,102 @@ func (u *GroupUpsert) ClearImagePrice4k() *GroupUpsert {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice360 sets the "sora_image_price_360" field.
|
||||||
|
func (u *GroupUpsert) SetSoraImagePrice360(v float64) *GroupUpsert {
|
||||||
|
u.Set(group.FieldSoraImagePrice360, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraImagePrice360 sets the "sora_image_price_360" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsert) UpdateSoraImagePrice360() *GroupUpsert {
|
||||||
|
u.SetExcluded(group.FieldSoraImagePrice360)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraImagePrice360 adds v to the "sora_image_price_360" field.
|
||||||
|
func (u *GroupUpsert) AddSoraImagePrice360(v float64) *GroupUpsert {
|
||||||
|
u.Add(group.FieldSoraImagePrice360, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraImagePrice360 clears the value of the "sora_image_price_360" field.
|
||||||
|
func (u *GroupUpsert) ClearSoraImagePrice360() *GroupUpsert {
|
||||||
|
u.SetNull(group.FieldSoraImagePrice360)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice540 sets the "sora_image_price_540" field.
|
||||||
|
func (u *GroupUpsert) SetSoraImagePrice540(v float64) *GroupUpsert {
|
||||||
|
u.Set(group.FieldSoraImagePrice540, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraImagePrice540 sets the "sora_image_price_540" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsert) UpdateSoraImagePrice540() *GroupUpsert {
|
||||||
|
u.SetExcluded(group.FieldSoraImagePrice540)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraImagePrice540 adds v to the "sora_image_price_540" field.
|
||||||
|
func (u *GroupUpsert) AddSoraImagePrice540(v float64) *GroupUpsert {
|
||||||
|
u.Add(group.FieldSoraImagePrice540, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraImagePrice540 clears the value of the "sora_image_price_540" field.
|
||||||
|
func (u *GroupUpsert) ClearSoraImagePrice540() *GroupUpsert {
|
||||||
|
u.SetNull(group.FieldSoraImagePrice540)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequest sets the "sora_video_price_per_request" field.
|
||||||
|
func (u *GroupUpsert) SetSoraVideoPricePerRequest(v float64) *GroupUpsert {
|
||||||
|
u.Set(group.FieldSoraVideoPricePerRequest, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraVideoPricePerRequest sets the "sora_video_price_per_request" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsert) UpdateSoraVideoPricePerRequest() *GroupUpsert {
|
||||||
|
u.SetExcluded(group.FieldSoraVideoPricePerRequest)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraVideoPricePerRequest adds v to the "sora_video_price_per_request" field.
|
||||||
|
func (u *GroupUpsert) AddSoraVideoPricePerRequest(v float64) *GroupUpsert {
|
||||||
|
u.Add(group.FieldSoraVideoPricePerRequest, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraVideoPricePerRequest clears the value of the "sora_video_price_per_request" field.
|
||||||
|
func (u *GroupUpsert) ClearSoraVideoPricePerRequest() *GroupUpsert {
|
||||||
|
u.SetNull(group.FieldSoraVideoPricePerRequest)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field.
|
||||||
|
func (u *GroupUpsert) SetSoraVideoPricePerRequestHd(v float64) *GroupUpsert {
|
||||||
|
u.Set(group.FieldSoraVideoPricePerRequestHd, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsert) UpdateSoraVideoPricePerRequestHd() *GroupUpsert {
|
||||||
|
u.SetExcluded(group.FieldSoraVideoPricePerRequestHd)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraVideoPricePerRequestHd adds v to the "sora_video_price_per_request_hd" field.
|
||||||
|
func (u *GroupUpsert) AddSoraVideoPricePerRequestHd(v float64) *GroupUpsert {
|
||||||
|
u.Add(group.FieldSoraVideoPricePerRequestHd, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraVideoPricePerRequestHd clears the value of the "sora_video_price_per_request_hd" field.
|
||||||
|
func (u *GroupUpsert) ClearSoraVideoPricePerRequestHd() *GroupUpsert {
|
||||||
|
u.SetNull(group.FieldSoraVideoPricePerRequestHd)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||||
func (u *GroupUpsert) SetClaudeCodeOnly(v bool) *GroupUpsert {
|
func (u *GroupUpsert) SetClaudeCodeOnly(v bool) *GroupUpsert {
|
||||||
u.Set(group.FieldClaudeCodeOnly, v)
|
u.Set(group.FieldClaudeCodeOnly, v)
|
||||||
@@ -1690,6 +1858,118 @@ func (u *GroupUpsertOne) ClearImagePrice4k() *GroupUpsertOne {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice360 sets the "sora_image_price_360" field.
|
||||||
|
func (u *GroupUpsertOne) SetSoraImagePrice360(v float64) *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.SetSoraImagePrice360(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraImagePrice360 adds v to the "sora_image_price_360" field.
|
||||||
|
func (u *GroupUpsertOne) AddSoraImagePrice360(v float64) *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.AddSoraImagePrice360(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraImagePrice360 sets the "sora_image_price_360" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsertOne) UpdateSoraImagePrice360() *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.UpdateSoraImagePrice360()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraImagePrice360 clears the value of the "sora_image_price_360" field.
|
||||||
|
func (u *GroupUpsertOne) ClearSoraImagePrice360() *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.ClearSoraImagePrice360()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice540 sets the "sora_image_price_540" field.
|
||||||
|
func (u *GroupUpsertOne) SetSoraImagePrice540(v float64) *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.SetSoraImagePrice540(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraImagePrice540 adds v to the "sora_image_price_540" field.
|
||||||
|
func (u *GroupUpsertOne) AddSoraImagePrice540(v float64) *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.AddSoraImagePrice540(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraImagePrice540 sets the "sora_image_price_540" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsertOne) UpdateSoraImagePrice540() *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.UpdateSoraImagePrice540()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraImagePrice540 clears the value of the "sora_image_price_540" field.
|
||||||
|
func (u *GroupUpsertOne) ClearSoraImagePrice540() *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.ClearSoraImagePrice540()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequest sets the "sora_video_price_per_request" field.
|
||||||
|
func (u *GroupUpsertOne) SetSoraVideoPricePerRequest(v float64) *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.SetSoraVideoPricePerRequest(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraVideoPricePerRequest adds v to the "sora_video_price_per_request" field.
|
||||||
|
func (u *GroupUpsertOne) AddSoraVideoPricePerRequest(v float64) *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.AddSoraVideoPricePerRequest(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraVideoPricePerRequest sets the "sora_video_price_per_request" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsertOne) UpdateSoraVideoPricePerRequest() *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.UpdateSoraVideoPricePerRequest()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraVideoPricePerRequest clears the value of the "sora_video_price_per_request" field.
|
||||||
|
func (u *GroupUpsertOne) ClearSoraVideoPricePerRequest() *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.ClearSoraVideoPricePerRequest()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field.
|
||||||
|
func (u *GroupUpsertOne) SetSoraVideoPricePerRequestHd(v float64) *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.SetSoraVideoPricePerRequestHd(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraVideoPricePerRequestHd adds v to the "sora_video_price_per_request_hd" field.
|
||||||
|
func (u *GroupUpsertOne) AddSoraVideoPricePerRequestHd(v float64) *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.AddSoraVideoPricePerRequestHd(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsertOne) UpdateSoraVideoPricePerRequestHd() *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.UpdateSoraVideoPricePerRequestHd()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraVideoPricePerRequestHd clears the value of the "sora_video_price_per_request_hd" field.
|
||||||
|
func (u *GroupUpsertOne) ClearSoraVideoPricePerRequestHd() *GroupUpsertOne {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.ClearSoraVideoPricePerRequestHd()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||||
func (u *GroupUpsertOne) SetClaudeCodeOnly(v bool) *GroupUpsertOne {
|
func (u *GroupUpsertOne) SetClaudeCodeOnly(v bool) *GroupUpsertOne {
|
||||||
return u.Update(func(s *GroupUpsert) {
|
return u.Update(func(s *GroupUpsert) {
|
||||||
@@ -2391,6 +2671,118 @@ func (u *GroupUpsertBulk) ClearImagePrice4k() *GroupUpsertBulk {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice360 sets the "sora_image_price_360" field.
|
||||||
|
func (u *GroupUpsertBulk) SetSoraImagePrice360(v float64) *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.SetSoraImagePrice360(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraImagePrice360 adds v to the "sora_image_price_360" field.
|
||||||
|
func (u *GroupUpsertBulk) AddSoraImagePrice360(v float64) *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.AddSoraImagePrice360(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraImagePrice360 sets the "sora_image_price_360" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsertBulk) UpdateSoraImagePrice360() *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.UpdateSoraImagePrice360()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraImagePrice360 clears the value of the "sora_image_price_360" field.
|
||||||
|
func (u *GroupUpsertBulk) ClearSoraImagePrice360() *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.ClearSoraImagePrice360()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice540 sets the "sora_image_price_540" field.
|
||||||
|
func (u *GroupUpsertBulk) SetSoraImagePrice540(v float64) *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.SetSoraImagePrice540(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraImagePrice540 adds v to the "sora_image_price_540" field.
|
||||||
|
func (u *GroupUpsertBulk) AddSoraImagePrice540(v float64) *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.AddSoraImagePrice540(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraImagePrice540 sets the "sora_image_price_540" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsertBulk) UpdateSoraImagePrice540() *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.UpdateSoraImagePrice540()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraImagePrice540 clears the value of the "sora_image_price_540" field.
|
||||||
|
func (u *GroupUpsertBulk) ClearSoraImagePrice540() *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.ClearSoraImagePrice540()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequest sets the "sora_video_price_per_request" field.
|
||||||
|
func (u *GroupUpsertBulk) SetSoraVideoPricePerRequest(v float64) *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.SetSoraVideoPricePerRequest(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraVideoPricePerRequest adds v to the "sora_video_price_per_request" field.
|
||||||
|
func (u *GroupUpsertBulk) AddSoraVideoPricePerRequest(v float64) *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.AddSoraVideoPricePerRequest(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraVideoPricePerRequest sets the "sora_video_price_per_request" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsertBulk) UpdateSoraVideoPricePerRequest() *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.UpdateSoraVideoPricePerRequest()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraVideoPricePerRequest clears the value of the "sora_video_price_per_request" field.
|
||||||
|
func (u *GroupUpsertBulk) ClearSoraVideoPricePerRequest() *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.ClearSoraVideoPricePerRequest()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field.
|
||||||
|
func (u *GroupUpsertBulk) SetSoraVideoPricePerRequestHd(v float64) *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.SetSoraVideoPricePerRequestHd(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraVideoPricePerRequestHd adds v to the "sora_video_price_per_request_hd" field.
|
||||||
|
func (u *GroupUpsertBulk) AddSoraVideoPricePerRequestHd(v float64) *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.AddSoraVideoPricePerRequestHd(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field to the value that was provided on create.
|
||||||
|
func (u *GroupUpsertBulk) UpdateSoraVideoPricePerRequestHd() *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.UpdateSoraVideoPricePerRequestHd()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraVideoPricePerRequestHd clears the value of the "sora_video_price_per_request_hd" field.
|
||||||
|
func (u *GroupUpsertBulk) ClearSoraVideoPricePerRequestHd() *GroupUpsertBulk {
|
||||||
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
s.ClearSoraVideoPricePerRequestHd()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||||
func (u *GroupUpsertBulk) SetClaudeCodeOnly(v bool) *GroupUpsertBulk {
|
func (u *GroupUpsertBulk) SetClaudeCodeOnly(v bool) *GroupUpsertBulk {
|
||||||
return u.Update(func(s *GroupUpsert) {
|
return u.Update(func(s *GroupUpsert) {
|
||||||
|
|||||||
@@ -355,6 +355,114 @@ func (_u *GroupUpdate) ClearImagePrice4k() *GroupUpdate {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice360 sets the "sora_image_price_360" field.
|
||||||
|
func (_u *GroupUpdate) SetSoraImagePrice360(v float64) *GroupUpdate {
|
||||||
|
_u.mutation.ResetSoraImagePrice360()
|
||||||
|
_u.mutation.SetSoraImagePrice360(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraImagePrice360 sets the "sora_image_price_360" field if the given value is not nil.
|
||||||
|
func (_u *GroupUpdate) SetNillableSoraImagePrice360(v *float64) *GroupUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetSoraImagePrice360(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraImagePrice360 adds value to the "sora_image_price_360" field.
|
||||||
|
func (_u *GroupUpdate) AddSoraImagePrice360(v float64) *GroupUpdate {
|
||||||
|
_u.mutation.AddSoraImagePrice360(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraImagePrice360 clears the value of the "sora_image_price_360" field.
|
||||||
|
func (_u *GroupUpdate) ClearSoraImagePrice360() *GroupUpdate {
|
||||||
|
_u.mutation.ClearSoraImagePrice360()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice540 sets the "sora_image_price_540" field.
|
||||||
|
func (_u *GroupUpdate) SetSoraImagePrice540(v float64) *GroupUpdate {
|
||||||
|
_u.mutation.ResetSoraImagePrice540()
|
||||||
|
_u.mutation.SetSoraImagePrice540(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraImagePrice540 sets the "sora_image_price_540" field if the given value is not nil.
|
||||||
|
func (_u *GroupUpdate) SetNillableSoraImagePrice540(v *float64) *GroupUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetSoraImagePrice540(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraImagePrice540 adds value to the "sora_image_price_540" field.
|
||||||
|
func (_u *GroupUpdate) AddSoraImagePrice540(v float64) *GroupUpdate {
|
||||||
|
_u.mutation.AddSoraImagePrice540(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraImagePrice540 clears the value of the "sora_image_price_540" field.
|
||||||
|
func (_u *GroupUpdate) ClearSoraImagePrice540() *GroupUpdate {
|
||||||
|
_u.mutation.ClearSoraImagePrice540()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequest sets the "sora_video_price_per_request" field.
|
||||||
|
func (_u *GroupUpdate) SetSoraVideoPricePerRequest(v float64) *GroupUpdate {
|
||||||
|
_u.mutation.ResetSoraVideoPricePerRequest()
|
||||||
|
_u.mutation.SetSoraVideoPricePerRequest(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraVideoPricePerRequest sets the "sora_video_price_per_request" field if the given value is not nil.
|
||||||
|
func (_u *GroupUpdate) SetNillableSoraVideoPricePerRequest(v *float64) *GroupUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetSoraVideoPricePerRequest(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraVideoPricePerRequest adds value to the "sora_video_price_per_request" field.
|
||||||
|
func (_u *GroupUpdate) AddSoraVideoPricePerRequest(v float64) *GroupUpdate {
|
||||||
|
_u.mutation.AddSoraVideoPricePerRequest(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraVideoPricePerRequest clears the value of the "sora_video_price_per_request" field.
|
||||||
|
func (_u *GroupUpdate) ClearSoraVideoPricePerRequest() *GroupUpdate {
|
||||||
|
_u.mutation.ClearSoraVideoPricePerRequest()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field.
|
||||||
|
func (_u *GroupUpdate) SetSoraVideoPricePerRequestHd(v float64) *GroupUpdate {
|
||||||
|
_u.mutation.ResetSoraVideoPricePerRequestHd()
|
||||||
|
_u.mutation.SetSoraVideoPricePerRequestHd(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field if the given value is not nil.
|
||||||
|
func (_u *GroupUpdate) SetNillableSoraVideoPricePerRequestHd(v *float64) *GroupUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetSoraVideoPricePerRequestHd(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraVideoPricePerRequestHd adds value to the "sora_video_price_per_request_hd" field.
|
||||||
|
func (_u *GroupUpdate) AddSoraVideoPricePerRequestHd(v float64) *GroupUpdate {
|
||||||
|
_u.mutation.AddSoraVideoPricePerRequestHd(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraVideoPricePerRequestHd clears the value of the "sora_video_price_per_request_hd" field.
|
||||||
|
func (_u *GroupUpdate) ClearSoraVideoPricePerRequestHd() *GroupUpdate {
|
||||||
|
_u.mutation.ClearSoraVideoPricePerRequestHd()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||||
func (_u *GroupUpdate) SetClaudeCodeOnly(v bool) *GroupUpdate {
|
func (_u *GroupUpdate) SetClaudeCodeOnly(v bool) *GroupUpdate {
|
||||||
_u.mutation.SetClaudeCodeOnly(v)
|
_u.mutation.SetClaudeCodeOnly(v)
|
||||||
@@ -892,6 +1000,42 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
|||||||
if _u.mutation.ImagePrice4kCleared() {
|
if _u.mutation.ImagePrice4kCleared() {
|
||||||
_spec.ClearField(group.FieldImagePrice4k, field.TypeFloat64)
|
_spec.ClearField(group.FieldImagePrice4k, field.TypeFloat64)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.SoraImagePrice360(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraImagePrice360, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AddedSoraImagePrice360(); ok {
|
||||||
|
_spec.AddField(group.FieldSoraImagePrice360, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.SoraImagePrice360Cleared() {
|
||||||
|
_spec.ClearField(group.FieldSoraImagePrice360, field.TypeFloat64)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.SoraImagePrice540(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraImagePrice540, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AddedSoraImagePrice540(); ok {
|
||||||
|
_spec.AddField(group.FieldSoraImagePrice540, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.SoraImagePrice540Cleared() {
|
||||||
|
_spec.ClearField(group.FieldSoraImagePrice540, field.TypeFloat64)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.SoraVideoPricePerRequest(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraVideoPricePerRequest, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AddedSoraVideoPricePerRequest(); ok {
|
||||||
|
_spec.AddField(group.FieldSoraVideoPricePerRequest, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.SoraVideoPricePerRequestCleared() {
|
||||||
|
_spec.ClearField(group.FieldSoraVideoPricePerRequest, field.TypeFloat64)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.SoraVideoPricePerRequestHd(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraVideoPricePerRequestHd, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AddedSoraVideoPricePerRequestHd(); ok {
|
||||||
|
_spec.AddField(group.FieldSoraVideoPricePerRequestHd, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.SoraVideoPricePerRequestHdCleared() {
|
||||||
|
_spec.ClearField(group.FieldSoraVideoPricePerRequestHd, field.TypeFloat64)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.ClaudeCodeOnly(); ok {
|
if value, ok := _u.mutation.ClaudeCodeOnly(); ok {
|
||||||
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
|
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
|
||||||
}
|
}
|
||||||
@@ -1573,6 +1717,114 @@ func (_u *GroupUpdateOne) ClearImagePrice4k() *GroupUpdateOne {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice360 sets the "sora_image_price_360" field.
|
||||||
|
func (_u *GroupUpdateOne) SetSoraImagePrice360(v float64) *GroupUpdateOne {
|
||||||
|
_u.mutation.ResetSoraImagePrice360()
|
||||||
|
_u.mutation.SetSoraImagePrice360(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraImagePrice360 sets the "sora_image_price_360" field if the given value is not nil.
|
||||||
|
func (_u *GroupUpdateOne) SetNillableSoraImagePrice360(v *float64) *GroupUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetSoraImagePrice360(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraImagePrice360 adds value to the "sora_image_price_360" field.
|
||||||
|
func (_u *GroupUpdateOne) AddSoraImagePrice360(v float64) *GroupUpdateOne {
|
||||||
|
_u.mutation.AddSoraImagePrice360(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraImagePrice360 clears the value of the "sora_image_price_360" field.
|
||||||
|
func (_u *GroupUpdateOne) ClearSoraImagePrice360() *GroupUpdateOne {
|
||||||
|
_u.mutation.ClearSoraImagePrice360()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraImagePrice540 sets the "sora_image_price_540" field.
|
||||||
|
func (_u *GroupUpdateOne) SetSoraImagePrice540(v float64) *GroupUpdateOne {
|
||||||
|
_u.mutation.ResetSoraImagePrice540()
|
||||||
|
_u.mutation.SetSoraImagePrice540(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraImagePrice540 sets the "sora_image_price_540" field if the given value is not nil.
|
||||||
|
func (_u *GroupUpdateOne) SetNillableSoraImagePrice540(v *float64) *GroupUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetSoraImagePrice540(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraImagePrice540 adds value to the "sora_image_price_540" field.
|
||||||
|
func (_u *GroupUpdateOne) AddSoraImagePrice540(v float64) *GroupUpdateOne {
|
||||||
|
_u.mutation.AddSoraImagePrice540(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraImagePrice540 clears the value of the "sora_image_price_540" field.
|
||||||
|
func (_u *GroupUpdateOne) ClearSoraImagePrice540() *GroupUpdateOne {
|
||||||
|
_u.mutation.ClearSoraImagePrice540()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequest sets the "sora_video_price_per_request" field.
|
||||||
|
func (_u *GroupUpdateOne) SetSoraVideoPricePerRequest(v float64) *GroupUpdateOne {
|
||||||
|
_u.mutation.ResetSoraVideoPricePerRequest()
|
||||||
|
_u.mutation.SetSoraVideoPricePerRequest(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraVideoPricePerRequest sets the "sora_video_price_per_request" field if the given value is not nil.
|
||||||
|
func (_u *GroupUpdateOne) SetNillableSoraVideoPricePerRequest(v *float64) *GroupUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetSoraVideoPricePerRequest(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraVideoPricePerRequest adds value to the "sora_video_price_per_request" field.
|
||||||
|
func (_u *GroupUpdateOne) AddSoraVideoPricePerRequest(v float64) *GroupUpdateOne {
|
||||||
|
_u.mutation.AddSoraVideoPricePerRequest(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraVideoPricePerRequest clears the value of the "sora_video_price_per_request" field.
|
||||||
|
func (_u *GroupUpdateOne) ClearSoraVideoPricePerRequest() *GroupUpdateOne {
|
||||||
|
_u.mutation.ClearSoraVideoPricePerRequest()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field.
|
||||||
|
func (_u *GroupUpdateOne) SetSoraVideoPricePerRequestHd(v float64) *GroupUpdateOne {
|
||||||
|
_u.mutation.ResetSoraVideoPricePerRequestHd()
|
||||||
|
_u.mutation.SetSoraVideoPricePerRequestHd(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableSoraVideoPricePerRequestHd sets the "sora_video_price_per_request_hd" field if the given value is not nil.
|
||||||
|
func (_u *GroupUpdateOne) SetNillableSoraVideoPricePerRequestHd(v *float64) *GroupUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetSoraVideoPricePerRequestHd(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSoraVideoPricePerRequestHd adds value to the "sora_video_price_per_request_hd" field.
|
||||||
|
func (_u *GroupUpdateOne) AddSoraVideoPricePerRequestHd(v float64) *GroupUpdateOne {
|
||||||
|
_u.mutation.AddSoraVideoPricePerRequestHd(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSoraVideoPricePerRequestHd clears the value of the "sora_video_price_per_request_hd" field.
|
||||||
|
func (_u *GroupUpdateOne) ClearSoraVideoPricePerRequestHd() *GroupUpdateOne {
|
||||||
|
_u.mutation.ClearSoraVideoPricePerRequestHd()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
// SetClaudeCodeOnly sets the "claude_code_only" field.
|
||||||
func (_u *GroupUpdateOne) SetClaudeCodeOnly(v bool) *GroupUpdateOne {
|
func (_u *GroupUpdateOne) SetClaudeCodeOnly(v bool) *GroupUpdateOne {
|
||||||
_u.mutation.SetClaudeCodeOnly(v)
|
_u.mutation.SetClaudeCodeOnly(v)
|
||||||
@@ -2140,6 +2392,42 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
|
|||||||
if _u.mutation.ImagePrice4kCleared() {
|
if _u.mutation.ImagePrice4kCleared() {
|
||||||
_spec.ClearField(group.FieldImagePrice4k, field.TypeFloat64)
|
_spec.ClearField(group.FieldImagePrice4k, field.TypeFloat64)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.SoraImagePrice360(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraImagePrice360, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AddedSoraImagePrice360(); ok {
|
||||||
|
_spec.AddField(group.FieldSoraImagePrice360, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.SoraImagePrice360Cleared() {
|
||||||
|
_spec.ClearField(group.FieldSoraImagePrice360, field.TypeFloat64)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.SoraImagePrice540(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraImagePrice540, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AddedSoraImagePrice540(); ok {
|
||||||
|
_spec.AddField(group.FieldSoraImagePrice540, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.SoraImagePrice540Cleared() {
|
||||||
|
_spec.ClearField(group.FieldSoraImagePrice540, field.TypeFloat64)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.SoraVideoPricePerRequest(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraVideoPricePerRequest, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AddedSoraVideoPricePerRequest(); ok {
|
||||||
|
_spec.AddField(group.FieldSoraVideoPricePerRequest, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.SoraVideoPricePerRequestCleared() {
|
||||||
|
_spec.ClearField(group.FieldSoraVideoPricePerRequest, field.TypeFloat64)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.SoraVideoPricePerRequestHd(); ok {
|
||||||
|
_spec.SetField(group.FieldSoraVideoPricePerRequestHd, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AddedSoraVideoPricePerRequestHd(); ok {
|
||||||
|
_spec.AddField(group.FieldSoraVideoPricePerRequestHd, field.TypeFloat64, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.SoraVideoPricePerRequestHdCleared() {
|
||||||
|
_spec.ClearField(group.FieldSoraVideoPricePerRequestHd, field.TypeFloat64)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.ClaudeCodeOnly(); ok {
|
if value, ok := _u.mutation.ClaudeCodeOnly(); ok {
|
||||||
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
|
_spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,18 @@ func (f RedeemCodeFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value,
|
|||||||
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.RedeemCodeMutation", m)
|
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.RedeemCodeMutation", m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The SecuritySecretFunc type is an adapter to allow the use of ordinary
|
||||||
|
// function as SecuritySecret mutator.
|
||||||
|
type SecuritySecretFunc func(context.Context, *ent.SecuritySecretMutation) (ent.Value, error)
|
||||||
|
|
||||||
|
// Mutate calls f(ctx, m).
|
||||||
|
func (f SecuritySecretFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) {
|
||||||
|
if mv, ok := m.(*ent.SecuritySecretMutation); ok {
|
||||||
|
return f(ctx, mv)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.SecuritySecretMutation", m)
|
||||||
|
}
|
||||||
|
|
||||||
// The SettingFunc type is an adapter to allow the use of ordinary
|
// The SettingFunc type is an adapter to allow the use of ordinary
|
||||||
// function as Setting mutator.
|
// function as Setting mutator.
|
||||||
type SettingFunc func(context.Context, *ent.SettingMutation) (ent.Value, error)
|
type SettingFunc func(context.Context, *ent.SettingMutation) (ent.Value, error)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
|
"github.com/Wei-Shaw/sub2api/ent/promocodeusage"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/proxy"
|
"github.com/Wei-Shaw/sub2api/ent/proxy"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
"github.com/Wei-Shaw/sub2api/ent/setting"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||||
@@ -383,6 +384,33 @@ func (f TraverseRedeemCode) Traverse(ctx context.Context, q ent.Query) error {
|
|||||||
return fmt.Errorf("unexpected query type %T. expect *ent.RedeemCodeQuery", q)
|
return fmt.Errorf("unexpected query type %T. expect *ent.RedeemCodeQuery", q)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The SecuritySecretFunc type is an adapter to allow the use of ordinary function as a Querier.
|
||||||
|
type SecuritySecretFunc func(context.Context, *ent.SecuritySecretQuery) (ent.Value, error)
|
||||||
|
|
||||||
|
// Query calls f(ctx, q).
|
||||||
|
func (f SecuritySecretFunc) Query(ctx context.Context, q ent.Query) (ent.Value, error) {
|
||||||
|
if q, ok := q.(*ent.SecuritySecretQuery); ok {
|
||||||
|
return f(ctx, q)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected query type %T. expect *ent.SecuritySecretQuery", q)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The TraverseSecuritySecret type is an adapter to allow the use of ordinary function as Traverser.
|
||||||
|
type TraverseSecuritySecret func(context.Context, *ent.SecuritySecretQuery) error
|
||||||
|
|
||||||
|
// Intercept is a dummy implementation of Intercept that returns the next Querier in the pipeline.
|
||||||
|
func (f TraverseSecuritySecret) Intercept(next ent.Querier) ent.Querier {
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traverse calls f(ctx, q).
|
||||||
|
func (f TraverseSecuritySecret) Traverse(ctx context.Context, q ent.Query) error {
|
||||||
|
if q, ok := q.(*ent.SecuritySecretQuery); ok {
|
||||||
|
return f(ctx, q)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unexpected query type %T. expect *ent.SecuritySecretQuery", q)
|
||||||
|
}
|
||||||
|
|
||||||
// The SettingFunc type is an adapter to allow the use of ordinary function as a Querier.
|
// The SettingFunc type is an adapter to allow the use of ordinary function as a Querier.
|
||||||
type SettingFunc func(context.Context, *ent.SettingQuery) (ent.Value, error)
|
type SettingFunc func(context.Context, *ent.SettingQuery) (ent.Value, error)
|
||||||
|
|
||||||
@@ -624,6 +652,8 @@ func NewQuery(q ent.Query) (Query, error) {
|
|||||||
return &query[*ent.ProxyQuery, predicate.Proxy, proxy.OrderOption]{typ: ent.TypeProxy, tq: q}, nil
|
return &query[*ent.ProxyQuery, predicate.Proxy, proxy.OrderOption]{typ: ent.TypeProxy, tq: q}, nil
|
||||||
case *ent.RedeemCodeQuery:
|
case *ent.RedeemCodeQuery:
|
||||||
return &query[*ent.RedeemCodeQuery, predicate.RedeemCode, redeemcode.OrderOption]{typ: ent.TypeRedeemCode, tq: q}, nil
|
return &query[*ent.RedeemCodeQuery, predicate.RedeemCode, redeemcode.OrderOption]{typ: ent.TypeRedeemCode, tq: q}, nil
|
||||||
|
case *ent.SecuritySecretQuery:
|
||||||
|
return &query[*ent.SecuritySecretQuery, predicate.SecuritySecret, securitysecret.OrderOption]{typ: ent.TypeSecuritySecret, tq: q}, nil
|
||||||
case *ent.SettingQuery:
|
case *ent.SettingQuery:
|
||||||
return &query[*ent.SettingQuery, predicate.Setting, setting.OrderOption]{typ: ent.TypeSetting, tq: q}, nil
|
return &query[*ent.SettingQuery, predicate.Setting, setting.OrderOption]{typ: ent.TypeSetting, tq: q}, nil
|
||||||
case *ent.UsageCleanupTaskQuery:
|
case *ent.UsageCleanupTaskQuery:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var (
|
|||||||
{Name: "key", Type: field.TypeString, Unique: true, Size: 128},
|
{Name: "key", Type: field.TypeString, Unique: true, Size: 128},
|
||||||
{Name: "name", Type: field.TypeString, Size: 100},
|
{Name: "name", Type: field.TypeString, Size: 100},
|
||||||
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
|
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
|
||||||
|
{Name: "last_used_at", Type: field.TypeTime, Nullable: true},
|
||||||
{Name: "ip_whitelist", Type: field.TypeJSON, Nullable: true},
|
{Name: "ip_whitelist", Type: field.TypeJSON, Nullable: true},
|
||||||
{Name: "ip_blacklist", Type: field.TypeJSON, Nullable: true},
|
{Name: "ip_blacklist", Type: field.TypeJSON, Nullable: true},
|
||||||
{Name: "quota", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
{Name: "quota", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||||
@@ -34,13 +35,13 @@ var (
|
|||||||
ForeignKeys: []*schema.ForeignKey{
|
ForeignKeys: []*schema.ForeignKey{
|
||||||
{
|
{
|
||||||
Symbol: "api_keys_groups_api_keys",
|
Symbol: "api_keys_groups_api_keys",
|
||||||
Columns: []*schema.Column{APIKeysColumns[12]},
|
Columns: []*schema.Column{APIKeysColumns[13]},
|
||||||
RefColumns: []*schema.Column{GroupsColumns[0]},
|
RefColumns: []*schema.Column{GroupsColumns[0]},
|
||||||
OnDelete: schema.SetNull,
|
OnDelete: schema.SetNull,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: "api_keys_users_api_keys",
|
Symbol: "api_keys_users_api_keys",
|
||||||
Columns: []*schema.Column{APIKeysColumns[13]},
|
Columns: []*schema.Column{APIKeysColumns[14]},
|
||||||
RefColumns: []*schema.Column{UsersColumns[0]},
|
RefColumns: []*schema.Column{UsersColumns[0]},
|
||||||
OnDelete: schema.NoAction,
|
OnDelete: schema.NoAction,
|
||||||
},
|
},
|
||||||
@@ -49,12 +50,12 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "apikey_user_id",
|
Name: "apikey_user_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{APIKeysColumns[13]},
|
Columns: []*schema.Column{APIKeysColumns[14]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "apikey_group_id",
|
Name: "apikey_group_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{APIKeysColumns[12]},
|
Columns: []*schema.Column{APIKeysColumns[13]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "apikey_status",
|
Name: "apikey_status",
|
||||||
@@ -66,15 +67,20 @@ var (
|
|||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{APIKeysColumns[3]},
|
Columns: []*schema.Column{APIKeysColumns[3]},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "apikey_last_used_at",
|
||||||
|
Unique: false,
|
||||||
|
Columns: []*schema.Column{APIKeysColumns[7]},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "apikey_quota_quota_used",
|
Name: "apikey_quota_quota_used",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{APIKeysColumns[9], APIKeysColumns[10]},
|
Columns: []*schema.Column{APIKeysColumns[10], APIKeysColumns[11]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "apikey_expires_at",
|
Name: "apikey_expires_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{APIKeysColumns[11]},
|
Columns: []*schema.Column{APIKeysColumns[12]},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -325,6 +331,7 @@ var (
|
|||||||
{Name: "response_code", Type: field.TypeInt, Nullable: true},
|
{Name: "response_code", Type: field.TypeInt, Nullable: true},
|
||||||
{Name: "passthrough_body", Type: field.TypeBool, Default: true},
|
{Name: "passthrough_body", Type: field.TypeBool, Default: true},
|
||||||
{Name: "custom_message", Type: field.TypeString, Nullable: true, Size: 2147483647},
|
{Name: "custom_message", Type: field.TypeString, Nullable: true, Size: 2147483647},
|
||||||
|
{Name: "skip_monitoring", Type: field.TypeBool, Default: false},
|
||||||
{Name: "description", Type: field.TypeString, Nullable: true, Size: 2147483647},
|
{Name: "description", Type: field.TypeString, Nullable: true, Size: 2147483647},
|
||||||
}
|
}
|
||||||
// ErrorPassthroughRulesTable holds the schema information for the "error_passthrough_rules" table.
|
// ErrorPassthroughRulesTable holds the schema information for the "error_passthrough_rules" table.
|
||||||
@@ -365,6 +372,10 @@ var (
|
|||||||
{Name: "image_price_1k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
{Name: "image_price_1k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||||
{Name: "image_price_2k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
{Name: "image_price_2k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||||
{Name: "image_price_4k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
{Name: "image_price_4k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||||
|
{Name: "sora_image_price_360", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||||
|
{Name: "sora_image_price_540", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||||
|
{Name: "sora_video_price_per_request", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||||
|
{Name: "sora_video_price_per_request_hd", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||||
{Name: "claude_code_only", Type: field.TypeBool, Default: false},
|
{Name: "claude_code_only", Type: field.TypeBool, Default: false},
|
||||||
{Name: "fallback_group_id", Type: field.TypeInt64, Nullable: true},
|
{Name: "fallback_group_id", Type: field.TypeInt64, Nullable: true},
|
||||||
{Name: "fallback_group_id_on_invalid_request", Type: field.TypeInt64, Nullable: true},
|
{Name: "fallback_group_id_on_invalid_request", Type: field.TypeInt64, Nullable: true},
|
||||||
@@ -408,7 +419,7 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "group_sort_order",
|
Name: "group_sort_order",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{GroupsColumns[25]},
|
Columns: []*schema.Column{GroupsColumns[29]},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -571,6 +582,20 @@ var (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// SecuritySecretsColumns holds the columns for the "security_secrets" table.
|
||||||
|
SecuritySecretsColumns = []*schema.Column{
|
||||||
|
{Name: "id", Type: field.TypeInt64, Increment: true},
|
||||||
|
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
|
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
|
{Name: "key", Type: field.TypeString, Unique: true, Size: 100},
|
||||||
|
{Name: "value", Type: field.TypeString, SchemaType: map[string]string{"postgres": "text"}},
|
||||||
|
}
|
||||||
|
// SecuritySecretsTable holds the schema information for the "security_secrets" table.
|
||||||
|
SecuritySecretsTable = &schema.Table{
|
||||||
|
Name: "security_secrets",
|
||||||
|
Columns: SecuritySecretsColumns,
|
||||||
|
PrimaryKey: []*schema.Column{SecuritySecretsColumns[0]},
|
||||||
|
}
|
||||||
// SettingsColumns holds the columns for the "settings" table.
|
// SettingsColumns holds the columns for the "settings" table.
|
||||||
SettingsColumns = []*schema.Column{
|
SettingsColumns = []*schema.Column{
|
||||||
{Name: "id", Type: field.TypeInt64, Increment: true},
|
{Name: "id", Type: field.TypeInt64, Increment: true},
|
||||||
@@ -649,6 +674,8 @@ var (
|
|||||||
{Name: "ip_address", Type: field.TypeString, Nullable: true, Size: 45},
|
{Name: "ip_address", Type: field.TypeString, Nullable: true, Size: 45},
|
||||||
{Name: "image_count", Type: field.TypeInt, Default: 0},
|
{Name: "image_count", Type: field.TypeInt, Default: 0},
|
||||||
{Name: "image_size", Type: field.TypeString, Nullable: true, Size: 10},
|
{Name: "image_size", Type: field.TypeString, Nullable: true, Size: 10},
|
||||||
|
{Name: "media_type", Type: field.TypeString, Nullable: true, Size: 16},
|
||||||
|
{Name: "cache_ttl_overridden", Type: field.TypeBool, Default: false},
|
||||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
{Name: "api_key_id", Type: field.TypeInt64},
|
{Name: "api_key_id", Type: field.TypeInt64},
|
||||||
{Name: "account_id", Type: field.TypeInt64},
|
{Name: "account_id", Type: field.TypeInt64},
|
||||||
@@ -664,31 +691,31 @@ var (
|
|||||||
ForeignKeys: []*schema.ForeignKey{
|
ForeignKeys: []*schema.ForeignKey{
|
||||||
{
|
{
|
||||||
Symbol: "usage_logs_api_keys_usage_logs",
|
Symbol: "usage_logs_api_keys_usage_logs",
|
||||||
Columns: []*schema.Column{UsageLogsColumns[26]},
|
Columns: []*schema.Column{UsageLogsColumns[28]},
|
||||||
RefColumns: []*schema.Column{APIKeysColumns[0]},
|
RefColumns: []*schema.Column{APIKeysColumns[0]},
|
||||||
OnDelete: schema.NoAction,
|
OnDelete: schema.NoAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: "usage_logs_accounts_usage_logs",
|
Symbol: "usage_logs_accounts_usage_logs",
|
||||||
Columns: []*schema.Column{UsageLogsColumns[27]},
|
Columns: []*schema.Column{UsageLogsColumns[29]},
|
||||||
RefColumns: []*schema.Column{AccountsColumns[0]},
|
RefColumns: []*schema.Column{AccountsColumns[0]},
|
||||||
OnDelete: schema.NoAction,
|
OnDelete: schema.NoAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: "usage_logs_groups_usage_logs",
|
Symbol: "usage_logs_groups_usage_logs",
|
||||||
Columns: []*schema.Column{UsageLogsColumns[28]},
|
Columns: []*schema.Column{UsageLogsColumns[30]},
|
||||||
RefColumns: []*schema.Column{GroupsColumns[0]},
|
RefColumns: []*schema.Column{GroupsColumns[0]},
|
||||||
OnDelete: schema.SetNull,
|
OnDelete: schema.SetNull,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: "usage_logs_users_usage_logs",
|
Symbol: "usage_logs_users_usage_logs",
|
||||||
Columns: []*schema.Column{UsageLogsColumns[29]},
|
Columns: []*schema.Column{UsageLogsColumns[31]},
|
||||||
RefColumns: []*schema.Column{UsersColumns[0]},
|
RefColumns: []*schema.Column{UsersColumns[0]},
|
||||||
OnDelete: schema.NoAction,
|
OnDelete: schema.NoAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: "usage_logs_user_subscriptions_usage_logs",
|
Symbol: "usage_logs_user_subscriptions_usage_logs",
|
||||||
Columns: []*schema.Column{UsageLogsColumns[30]},
|
Columns: []*schema.Column{UsageLogsColumns[32]},
|
||||||
RefColumns: []*schema.Column{UserSubscriptionsColumns[0]},
|
RefColumns: []*schema.Column{UserSubscriptionsColumns[0]},
|
||||||
OnDelete: schema.SetNull,
|
OnDelete: schema.SetNull,
|
||||||
},
|
},
|
||||||
@@ -697,32 +724,32 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "usagelog_user_id",
|
Name: "usagelog_user_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[29]},
|
Columns: []*schema.Column{UsageLogsColumns[31]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_api_key_id",
|
Name: "usagelog_api_key_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[26]},
|
Columns: []*schema.Column{UsageLogsColumns[28]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_account_id",
|
Name: "usagelog_account_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[27]},
|
Columns: []*schema.Column{UsageLogsColumns[29]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_group_id",
|
Name: "usagelog_group_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[28]},
|
Columns: []*schema.Column{UsageLogsColumns[30]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_subscription_id",
|
Name: "usagelog_subscription_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[30]},
|
Columns: []*schema.Column{UsageLogsColumns[32]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_created_at",
|
Name: "usagelog_created_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[25]},
|
Columns: []*schema.Column{UsageLogsColumns[27]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_model",
|
Name: "usagelog_model",
|
||||||
@@ -737,12 +764,12 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "usagelog_user_id_created_at",
|
Name: "usagelog_user_id_created_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[29], UsageLogsColumns[25]},
|
Columns: []*schema.Column{UsageLogsColumns[31], UsageLogsColumns[27]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "usagelog_api_key_id_created_at",
|
Name: "usagelog_api_key_id_created_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UsageLogsColumns[26], UsageLogsColumns[25]},
|
Columns: []*schema.Column{UsageLogsColumns[28], UsageLogsColumns[27]},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -998,6 +1025,7 @@ var (
|
|||||||
PromoCodeUsagesTable,
|
PromoCodeUsagesTable,
|
||||||
ProxiesTable,
|
ProxiesTable,
|
||||||
RedeemCodesTable,
|
RedeemCodesTable,
|
||||||
|
SecuritySecretsTable,
|
||||||
SettingsTable,
|
SettingsTable,
|
||||||
UsageCleanupTasksTable,
|
UsageCleanupTasksTable,
|
||||||
UsageLogsTable,
|
UsageLogsTable,
|
||||||
@@ -1054,6 +1082,9 @@ func init() {
|
|||||||
RedeemCodesTable.Annotation = &entsql.Annotation{
|
RedeemCodesTable.Annotation = &entsql.Annotation{
|
||||||
Table: "redeem_codes",
|
Table: "redeem_codes",
|
||||||
}
|
}
|
||||||
|
SecuritySecretsTable.Annotation = &entsql.Annotation{
|
||||||
|
Table: "security_secrets",
|
||||||
|
}
|
||||||
SettingsTable.Annotation = &entsql.Annotation{
|
SettingsTable.Annotation = &entsql.Annotation{
|
||||||
Table: "settings",
|
Table: "settings",
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,9 @@ type Proxy func(*sql.Selector)
|
|||||||
// RedeemCode is the predicate function for redeemcode builders.
|
// RedeemCode is the predicate function for redeemcode builders.
|
||||||
type RedeemCode func(*sql.Selector)
|
type RedeemCode func(*sql.Selector)
|
||||||
|
|
||||||
|
// SecuritySecret is the predicate function for securitysecret builders.
|
||||||
|
type SecuritySecret func(*sql.Selector)
|
||||||
|
|
||||||
// Setting is the predicate function for setting builders.
|
// Setting is the predicate function for setting builders.
|
||||||
type Setting func(*sql.Selector)
|
type Setting func(*sql.Selector)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/ent/proxy"
|
"github.com/Wei-Shaw/sub2api/ent/proxy"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
"github.com/Wei-Shaw/sub2api/ent/redeemcode"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/schema"
|
"github.com/Wei-Shaw/sub2api/ent/schema"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/setting"
|
"github.com/Wei-Shaw/sub2api/ent/setting"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
"github.com/Wei-Shaw/sub2api/ent/usagecleanuptask"
|
||||||
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
"github.com/Wei-Shaw/sub2api/ent/usagelog"
|
||||||
@@ -93,11 +94,11 @@ func init() {
|
|||||||
// apikey.StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
// apikey.StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
||||||
apikey.StatusValidator = apikeyDescStatus.Validators[0].(func(string) error)
|
apikey.StatusValidator = apikeyDescStatus.Validators[0].(func(string) error)
|
||||||
// apikeyDescQuota is the schema descriptor for quota field.
|
// apikeyDescQuota is the schema descriptor for quota field.
|
||||||
apikeyDescQuota := apikeyFields[7].Descriptor()
|
apikeyDescQuota := apikeyFields[8].Descriptor()
|
||||||
// apikey.DefaultQuota holds the default value on creation for the quota field.
|
// apikey.DefaultQuota holds the default value on creation for the quota field.
|
||||||
apikey.DefaultQuota = apikeyDescQuota.Default.(float64)
|
apikey.DefaultQuota = apikeyDescQuota.Default.(float64)
|
||||||
// apikeyDescQuotaUsed is the schema descriptor for quota_used field.
|
// apikeyDescQuotaUsed is the schema descriptor for quota_used field.
|
||||||
apikeyDescQuotaUsed := apikeyFields[8].Descriptor()
|
apikeyDescQuotaUsed := apikeyFields[9].Descriptor()
|
||||||
// apikey.DefaultQuotaUsed holds the default value on creation for the quota_used field.
|
// apikey.DefaultQuotaUsed holds the default value on creation for the quota_used field.
|
||||||
apikey.DefaultQuotaUsed = apikeyDescQuotaUsed.Default.(float64)
|
apikey.DefaultQuotaUsed = apikeyDescQuotaUsed.Default.(float64)
|
||||||
accountMixin := schema.Account{}.Mixin()
|
accountMixin := schema.Account{}.Mixin()
|
||||||
@@ -326,6 +327,10 @@ func init() {
|
|||||||
errorpassthroughruleDescPassthroughBody := errorpassthroughruleFields[9].Descriptor()
|
errorpassthroughruleDescPassthroughBody := errorpassthroughruleFields[9].Descriptor()
|
||||||
// errorpassthroughrule.DefaultPassthroughBody holds the default value on creation for the passthrough_body field.
|
// errorpassthroughrule.DefaultPassthroughBody holds the default value on creation for the passthrough_body field.
|
||||||
errorpassthroughrule.DefaultPassthroughBody = errorpassthroughruleDescPassthroughBody.Default.(bool)
|
errorpassthroughrule.DefaultPassthroughBody = errorpassthroughruleDescPassthroughBody.Default.(bool)
|
||||||
|
// errorpassthroughruleDescSkipMonitoring is the schema descriptor for skip_monitoring field.
|
||||||
|
errorpassthroughruleDescSkipMonitoring := errorpassthroughruleFields[11].Descriptor()
|
||||||
|
// errorpassthroughrule.DefaultSkipMonitoring holds the default value on creation for the skip_monitoring field.
|
||||||
|
errorpassthroughrule.DefaultSkipMonitoring = errorpassthroughruleDescSkipMonitoring.Default.(bool)
|
||||||
groupMixin := schema.Group{}.Mixin()
|
groupMixin := schema.Group{}.Mixin()
|
||||||
groupMixinHooks1 := groupMixin[1].Hooks()
|
groupMixinHooks1 := groupMixin[1].Hooks()
|
||||||
group.Hooks[0] = groupMixinHooks1[0]
|
group.Hooks[0] = groupMixinHooks1[0]
|
||||||
@@ -394,23 +399,23 @@ func init() {
|
|||||||
// group.DefaultDefaultValidityDays holds the default value on creation for the default_validity_days field.
|
// group.DefaultDefaultValidityDays holds the default value on creation for the default_validity_days field.
|
||||||
group.DefaultDefaultValidityDays = groupDescDefaultValidityDays.Default.(int)
|
group.DefaultDefaultValidityDays = groupDescDefaultValidityDays.Default.(int)
|
||||||
// groupDescClaudeCodeOnly is the schema descriptor for claude_code_only field.
|
// groupDescClaudeCodeOnly is the schema descriptor for claude_code_only field.
|
||||||
groupDescClaudeCodeOnly := groupFields[14].Descriptor()
|
groupDescClaudeCodeOnly := groupFields[18].Descriptor()
|
||||||
// group.DefaultClaudeCodeOnly holds the default value on creation for the claude_code_only field.
|
// group.DefaultClaudeCodeOnly holds the default value on creation for the claude_code_only field.
|
||||||
group.DefaultClaudeCodeOnly = groupDescClaudeCodeOnly.Default.(bool)
|
group.DefaultClaudeCodeOnly = groupDescClaudeCodeOnly.Default.(bool)
|
||||||
// groupDescModelRoutingEnabled is the schema descriptor for model_routing_enabled field.
|
// groupDescModelRoutingEnabled is the schema descriptor for model_routing_enabled field.
|
||||||
groupDescModelRoutingEnabled := groupFields[18].Descriptor()
|
groupDescModelRoutingEnabled := groupFields[22].Descriptor()
|
||||||
// group.DefaultModelRoutingEnabled holds the default value on creation for the model_routing_enabled field.
|
// group.DefaultModelRoutingEnabled holds the default value on creation for the model_routing_enabled field.
|
||||||
group.DefaultModelRoutingEnabled = groupDescModelRoutingEnabled.Default.(bool)
|
group.DefaultModelRoutingEnabled = groupDescModelRoutingEnabled.Default.(bool)
|
||||||
// groupDescMcpXMLInject is the schema descriptor for mcp_xml_inject field.
|
// groupDescMcpXMLInject is the schema descriptor for mcp_xml_inject field.
|
||||||
groupDescMcpXMLInject := groupFields[19].Descriptor()
|
groupDescMcpXMLInject := groupFields[23].Descriptor()
|
||||||
// group.DefaultMcpXMLInject holds the default value on creation for the mcp_xml_inject field.
|
// group.DefaultMcpXMLInject holds the default value on creation for the mcp_xml_inject field.
|
||||||
group.DefaultMcpXMLInject = groupDescMcpXMLInject.Default.(bool)
|
group.DefaultMcpXMLInject = groupDescMcpXMLInject.Default.(bool)
|
||||||
// groupDescSupportedModelScopes is the schema descriptor for supported_model_scopes field.
|
// groupDescSupportedModelScopes is the schema descriptor for supported_model_scopes field.
|
||||||
groupDescSupportedModelScopes := groupFields[20].Descriptor()
|
groupDescSupportedModelScopes := groupFields[24].Descriptor()
|
||||||
// group.DefaultSupportedModelScopes holds the default value on creation for the supported_model_scopes field.
|
// group.DefaultSupportedModelScopes holds the default value on creation for the supported_model_scopes field.
|
||||||
group.DefaultSupportedModelScopes = groupDescSupportedModelScopes.Default.([]string)
|
group.DefaultSupportedModelScopes = groupDescSupportedModelScopes.Default.([]string)
|
||||||
// groupDescSortOrder is the schema descriptor for sort_order field.
|
// groupDescSortOrder is the schema descriptor for sort_order field.
|
||||||
groupDescSortOrder := groupFields[21].Descriptor()
|
groupDescSortOrder := groupFields[25].Descriptor()
|
||||||
// group.DefaultSortOrder holds the default value on creation for the sort_order field.
|
// group.DefaultSortOrder holds the default value on creation for the sort_order field.
|
||||||
group.DefaultSortOrder = groupDescSortOrder.Default.(int)
|
group.DefaultSortOrder = groupDescSortOrder.Default.(int)
|
||||||
promocodeFields := schema.PromoCode{}.Fields()
|
promocodeFields := schema.PromoCode{}.Fields()
|
||||||
@@ -598,6 +603,43 @@ func init() {
|
|||||||
redeemcodeDescValidityDays := redeemcodeFields[9].Descriptor()
|
redeemcodeDescValidityDays := redeemcodeFields[9].Descriptor()
|
||||||
// redeemcode.DefaultValidityDays holds the default value on creation for the validity_days field.
|
// redeemcode.DefaultValidityDays holds the default value on creation for the validity_days field.
|
||||||
redeemcode.DefaultValidityDays = redeemcodeDescValidityDays.Default.(int)
|
redeemcode.DefaultValidityDays = redeemcodeDescValidityDays.Default.(int)
|
||||||
|
securitysecretMixin := schema.SecuritySecret{}.Mixin()
|
||||||
|
securitysecretMixinFields0 := securitysecretMixin[0].Fields()
|
||||||
|
_ = securitysecretMixinFields0
|
||||||
|
securitysecretFields := schema.SecuritySecret{}.Fields()
|
||||||
|
_ = securitysecretFields
|
||||||
|
// securitysecretDescCreatedAt is the schema descriptor for created_at field.
|
||||||
|
securitysecretDescCreatedAt := securitysecretMixinFields0[0].Descriptor()
|
||||||
|
// securitysecret.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||||
|
securitysecret.DefaultCreatedAt = securitysecretDescCreatedAt.Default.(func() time.Time)
|
||||||
|
// securitysecretDescUpdatedAt is the schema descriptor for updated_at field.
|
||||||
|
securitysecretDescUpdatedAt := securitysecretMixinFields0[1].Descriptor()
|
||||||
|
// securitysecret.DefaultUpdatedAt holds the default value on creation for the updated_at field.
|
||||||
|
securitysecret.DefaultUpdatedAt = securitysecretDescUpdatedAt.Default.(func() time.Time)
|
||||||
|
// securitysecret.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field.
|
||||||
|
securitysecret.UpdateDefaultUpdatedAt = securitysecretDescUpdatedAt.UpdateDefault.(func() time.Time)
|
||||||
|
// securitysecretDescKey is the schema descriptor for key field.
|
||||||
|
securitysecretDescKey := securitysecretFields[0].Descriptor()
|
||||||
|
// securitysecret.KeyValidator is a validator for the "key" field. It is called by the builders before save.
|
||||||
|
securitysecret.KeyValidator = func() func(string) error {
|
||||||
|
validators := securitysecretDescKey.Validators
|
||||||
|
fns := [...]func(string) error{
|
||||||
|
validators[0].(func(string) error),
|
||||||
|
validators[1].(func(string) error),
|
||||||
|
}
|
||||||
|
return func(key string) error {
|
||||||
|
for _, fn := range fns {
|
||||||
|
if err := fn(key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// securitysecretDescValue is the schema descriptor for value field.
|
||||||
|
securitysecretDescValue := securitysecretFields[1].Descriptor()
|
||||||
|
// securitysecret.ValueValidator is a validator for the "value" field. It is called by the builders before save.
|
||||||
|
securitysecret.ValueValidator = securitysecretDescValue.Validators[0].(func(string) error)
|
||||||
settingFields := schema.Setting{}.Fields()
|
settingFields := schema.Setting{}.Fields()
|
||||||
_ = settingFields
|
_ = settingFields
|
||||||
// settingDescKey is the schema descriptor for key field.
|
// settingDescKey is the schema descriptor for key field.
|
||||||
@@ -775,8 +817,16 @@ func init() {
|
|||||||
usagelogDescImageSize := usagelogFields[28].Descriptor()
|
usagelogDescImageSize := usagelogFields[28].Descriptor()
|
||||||
// usagelog.ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
|
// usagelog.ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
|
||||||
usagelog.ImageSizeValidator = usagelogDescImageSize.Validators[0].(func(string) error)
|
usagelog.ImageSizeValidator = usagelogDescImageSize.Validators[0].(func(string) error)
|
||||||
|
// usagelogDescMediaType is the schema descriptor for media_type field.
|
||||||
|
usagelogDescMediaType := usagelogFields[29].Descriptor()
|
||||||
|
// usagelog.MediaTypeValidator is a validator for the "media_type" field. It is called by the builders before save.
|
||||||
|
usagelog.MediaTypeValidator = usagelogDescMediaType.Validators[0].(func(string) error)
|
||||||
|
// usagelogDescCacheTTLOverridden is the schema descriptor for cache_ttl_overridden field.
|
||||||
|
usagelogDescCacheTTLOverridden := usagelogFields[30].Descriptor()
|
||||||
|
// usagelog.DefaultCacheTTLOverridden holds the default value on creation for the cache_ttl_overridden field.
|
||||||
|
usagelog.DefaultCacheTTLOverridden = usagelogDescCacheTTLOverridden.Default.(bool)
|
||||||
// usagelogDescCreatedAt is the schema descriptor for created_at field.
|
// usagelogDescCreatedAt is the schema descriptor for created_at field.
|
||||||
usagelogDescCreatedAt := usagelogFields[29].Descriptor()
|
usagelogDescCreatedAt := usagelogFields[31].Descriptor()
|
||||||
// usagelog.DefaultCreatedAt holds the default value on creation for the created_at field.
|
// usagelog.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||||
usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time)
|
usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time)
|
||||||
userMixin := schema.User{}.Mixin()
|
userMixin := schema.User{}.Mixin()
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ func (APIKey) Fields() []ent.Field {
|
|||||||
field.String("status").
|
field.String("status").
|
||||||
MaxLen(20).
|
MaxLen(20).
|
||||||
Default(domain.StatusActive),
|
Default(domain.StatusActive),
|
||||||
|
field.Time("last_used_at").
|
||||||
|
Optional().
|
||||||
|
Nillable().
|
||||||
|
Comment("Last usage time of this API key"),
|
||||||
field.JSON("ip_whitelist", []string{}).
|
field.JSON("ip_whitelist", []string{}).
|
||||||
Optional().
|
Optional().
|
||||||
Comment("Allowed IPs/CIDRs, e.g. [\"192.168.1.100\", \"10.0.0.0/8\"]"),
|
Comment("Allowed IPs/CIDRs, e.g. [\"192.168.1.100\", \"10.0.0.0/8\"]"),
|
||||||
@@ -95,6 +99,7 @@ func (APIKey) Indexes() []ent.Index {
|
|||||||
index.Fields("group_id"),
|
index.Fields("group_id"),
|
||||||
index.Fields("status"),
|
index.Fields("status"),
|
||||||
index.Fields("deleted_at"),
|
index.Fields("deleted_at"),
|
||||||
|
index.Fields("last_used_at"),
|
||||||
// Index for quota queries
|
// Index for quota queries
|
||||||
index.Fields("quota", "quota_used"),
|
index.Fields("quota", "quota_used"),
|
||||||
index.Fields("expires_at"),
|
index.Fields("expires_at"),
|
||||||
|
|||||||
@@ -105,6 +105,12 @@ func (ErrorPassthroughRule) Fields() []ent.Field {
|
|||||||
Optional().
|
Optional().
|
||||||
Nillable(),
|
Nillable(),
|
||||||
|
|
||||||
|
// skip_monitoring: 是否跳过运维监控记录
|
||||||
|
// true: 匹配此规则的错误不会被记录到 ops_error_logs
|
||||||
|
// false: 正常记录到运维监控(默认行为)
|
||||||
|
field.Bool("skip_monitoring").
|
||||||
|
Default(false),
|
||||||
|
|
||||||
// description: 规则描述,用于说明规则的用途
|
// description: 规则描述,用于说明规则的用途
|
||||||
field.Text("description").
|
field.Text("description").
|
||||||
Optional().
|
Optional().
|
||||||
|
|||||||
@@ -87,6 +87,24 @@ func (Group) Fields() []ent.Field {
|
|||||||
Nillable().
|
Nillable().
|
||||||
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
||||||
|
|
||||||
|
// Sora 按次计费配置(阶段 1)
|
||||||
|
field.Float("sora_image_price_360").
|
||||||
|
Optional().
|
||||||
|
Nillable().
|
||||||
|
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
||||||
|
field.Float("sora_image_price_540").
|
||||||
|
Optional().
|
||||||
|
Nillable().
|
||||||
|
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
||||||
|
field.Float("sora_video_price_per_request").
|
||||||
|
Optional().
|
||||||
|
Nillable().
|
||||||
|
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
||||||
|
field.Float("sora_video_price_per_request_hd").
|
||||||
|
Optional().
|
||||||
|
Nillable().
|
||||||
|
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
|
||||||
|
|
||||||
// Claude Code 客户端限制 (added by migration 029)
|
// Claude Code 客户端限制 (added by migration 029)
|
||||||
field.Bool("claude_code_only").
|
field.Bool("claude_code_only").
|
||||||
Default(false).
|
Default(false).
|
||||||
|
|||||||
50
backend/ent/schema/idempotency_record.go
Normal file
50
backend/ent/schema/idempotency_record.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||||
|
|
||||||
|
"entgo.io/ent"
|
||||||
|
"entgo.io/ent/dialect/entsql"
|
||||||
|
"entgo.io/ent/schema"
|
||||||
|
"entgo.io/ent/schema/field"
|
||||||
|
"entgo.io/ent/schema/index"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IdempotencyRecord 幂等请求记录表。
|
||||||
|
type IdempotencyRecord struct {
|
||||||
|
ent.Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
func (IdempotencyRecord) Annotations() []schema.Annotation {
|
||||||
|
return []schema.Annotation{
|
||||||
|
entsql.Annotation{Table: "idempotency_records"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (IdempotencyRecord) Mixin() []ent.Mixin {
|
||||||
|
return []ent.Mixin{
|
||||||
|
mixins.TimeMixin{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (IdempotencyRecord) Fields() []ent.Field {
|
||||||
|
return []ent.Field{
|
||||||
|
field.String("scope").MaxLen(128),
|
||||||
|
field.String("idempotency_key_hash").MaxLen(64),
|
||||||
|
field.String("request_fingerprint").MaxLen(64),
|
||||||
|
field.String("status").MaxLen(32),
|
||||||
|
field.Int("response_status").Optional().Nillable(),
|
||||||
|
field.String("response_body").Optional().Nillable(),
|
||||||
|
field.String("error_reason").MaxLen(128).Optional().Nillable(),
|
||||||
|
field.Time("locked_until").Optional().Nillable(),
|
||||||
|
field.Time("expires_at"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (IdempotencyRecord) Indexes() []ent.Index {
|
||||||
|
return []ent.Index{
|
||||||
|
index.Fields("scope", "idempotency_key_hash").Unique(),
|
||||||
|
index.Fields("expires_at"),
|
||||||
|
index.Fields("status", "locked_until"),
|
||||||
|
}
|
||||||
|
}
|
||||||
42
backend/ent/schema/security_secret.go
Normal file
42
backend/ent/schema/security_secret.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
|
||||||
|
|
||||||
|
"entgo.io/ent"
|
||||||
|
"entgo.io/ent/dialect"
|
||||||
|
"entgo.io/ent/dialect/entsql"
|
||||||
|
"entgo.io/ent/schema"
|
||||||
|
"entgo.io/ent/schema/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecuritySecret 存储系统级安全密钥(如 JWT 签名密钥、TOTP 加密密钥)。
|
||||||
|
type SecuritySecret struct {
|
||||||
|
ent.Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SecuritySecret) Annotations() []schema.Annotation {
|
||||||
|
return []schema.Annotation{
|
||||||
|
entsql.Annotation{Table: "security_secrets"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SecuritySecret) Mixin() []ent.Mixin {
|
||||||
|
return []ent.Mixin{
|
||||||
|
mixins.TimeMixin{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SecuritySecret) Fields() []ent.Field {
|
||||||
|
return []ent.Field{
|
||||||
|
field.String("key").
|
||||||
|
MaxLen(100).
|
||||||
|
NotEmpty().
|
||||||
|
Unique(),
|
||||||
|
field.String("value").
|
||||||
|
NotEmpty().
|
||||||
|
SchemaType(map[string]string{
|
||||||
|
dialect.Postgres: "text",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -118,6 +118,15 @@ func (UsageLog) Fields() []ent.Field {
|
|||||||
MaxLen(10).
|
MaxLen(10).
|
||||||
Optional().
|
Optional().
|
||||||
Nillable(),
|
Nillable(),
|
||||||
|
// 媒体类型字段(sora 使用)
|
||||||
|
field.String("media_type").
|
||||||
|
MaxLen(16).
|
||||||
|
Optional().
|
||||||
|
Nillable(),
|
||||||
|
|
||||||
|
// Cache TTL Override 标记(管理员强制替换了缓存 TTL 计费)
|
||||||
|
field.Bool("cache_ttl_overridden").
|
||||||
|
Default(false),
|
||||||
|
|
||||||
// 时间戳(只有 created_at,日志不可修改)
|
// 时间戳(只有 created_at,日志不可修改)
|
||||||
field.Time("created_at").
|
field.Time("created_at").
|
||||||
|
|||||||
139
backend/ent/securitysecret.go
Normal file
139
backend/ent/securitysecret.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// Code generated by ent, DO NOT EDIT.
|
||||||
|
|
||||||
|
package ent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"entgo.io/ent"
|
||||||
|
"entgo.io/ent/dialect/sql"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecuritySecret is the model entity for the SecuritySecret schema.
|
||||||
|
type SecuritySecret struct {
|
||||||
|
config `json:"-"`
|
||||||
|
// ID of the ent.
|
||||||
|
ID int64 `json:"id,omitempty"`
|
||||||
|
// CreatedAt holds the value of the "created_at" field.
|
||||||
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
|
// UpdatedAt holds the value of the "updated_at" field.
|
||||||
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||||
|
// Key holds the value of the "key" field.
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
// Value holds the value of the "value" field.
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
selectValues sql.SelectValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanValues returns the types for scanning values from sql.Rows.
|
||||||
|
func (*SecuritySecret) scanValues(columns []string) ([]any, error) {
|
||||||
|
values := make([]any, len(columns))
|
||||||
|
for i := range columns {
|
||||||
|
switch columns[i] {
|
||||||
|
case securitysecret.FieldID:
|
||||||
|
values[i] = new(sql.NullInt64)
|
||||||
|
case securitysecret.FieldKey, securitysecret.FieldValue:
|
||||||
|
values[i] = new(sql.NullString)
|
||||||
|
case securitysecret.FieldCreatedAt, securitysecret.FieldUpdatedAt:
|
||||||
|
values[i] = new(sql.NullTime)
|
||||||
|
default:
|
||||||
|
values[i] = new(sql.UnknownType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assignValues assigns the values that were returned from sql.Rows (after scanning)
|
||||||
|
// to the SecuritySecret fields.
|
||||||
|
func (_m *SecuritySecret) assignValues(columns []string, values []any) error {
|
||||||
|
if m, n := len(values), len(columns); m < n {
|
||||||
|
return fmt.Errorf("mismatch number of scan values: %d != %d", m, n)
|
||||||
|
}
|
||||||
|
for i := range columns {
|
||||||
|
switch columns[i] {
|
||||||
|
case securitysecret.FieldID:
|
||||||
|
value, ok := values[i].(*sql.NullInt64)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field id", value)
|
||||||
|
}
|
||||||
|
_m.ID = int64(value.Int64)
|
||||||
|
case securitysecret.FieldCreatedAt:
|
||||||
|
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field created_at", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.CreatedAt = value.Time
|
||||||
|
}
|
||||||
|
case securitysecret.FieldUpdatedAt:
|
||||||
|
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field updated_at", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.UpdatedAt = value.Time
|
||||||
|
}
|
||||||
|
case securitysecret.FieldKey:
|
||||||
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field key", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.Key = value.String
|
||||||
|
}
|
||||||
|
case securitysecret.FieldValue:
|
||||||
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field value", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.Value = value.String
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
_m.selectValues.Set(columns[i], values[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the ent.Value that was dynamically selected and assigned to the SecuritySecret.
|
||||||
|
// This includes values selected through modifiers, order, etc.
|
||||||
|
func (_m *SecuritySecret) GetValue(name string) (ent.Value, error) {
|
||||||
|
return _m.selectValues.Get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update returns a builder for updating this SecuritySecret.
|
||||||
|
// Note that you need to call SecuritySecret.Unwrap() before calling this method if this SecuritySecret
|
||||||
|
// was returned from a transaction, and the transaction was committed or rolled back.
|
||||||
|
func (_m *SecuritySecret) Update() *SecuritySecretUpdateOne {
|
||||||
|
return NewSecuritySecretClient(_m.config).UpdateOne(_m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap unwraps the SecuritySecret entity that was returned from a transaction after it was closed,
|
||||||
|
// so that all future queries will be executed through the driver which created the transaction.
|
||||||
|
func (_m *SecuritySecret) Unwrap() *SecuritySecret {
|
||||||
|
_tx, ok := _m.config.driver.(*txDriver)
|
||||||
|
if !ok {
|
||||||
|
panic("ent: SecuritySecret is not a transactional entity")
|
||||||
|
}
|
||||||
|
_m.config.driver = _tx.drv
|
||||||
|
return _m
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the fmt.Stringer.
|
||||||
|
func (_m *SecuritySecret) String() string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("SecuritySecret(")
|
||||||
|
builder.WriteString(fmt.Sprintf("id=%v, ", _m.ID))
|
||||||
|
builder.WriteString("created_at=")
|
||||||
|
builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
|
||||||
|
builder.WriteString(", ")
|
||||||
|
builder.WriteString("updated_at=")
|
||||||
|
builder.WriteString(_m.UpdatedAt.Format(time.ANSIC))
|
||||||
|
builder.WriteString(", ")
|
||||||
|
builder.WriteString("key=")
|
||||||
|
builder.WriteString(_m.Key)
|
||||||
|
builder.WriteString(", ")
|
||||||
|
builder.WriteString("value=")
|
||||||
|
builder.WriteString(_m.Value)
|
||||||
|
builder.WriteByte(')')
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecuritySecrets is a parsable slice of SecuritySecret.
|
||||||
|
type SecuritySecrets []*SecuritySecret
|
||||||
86
backend/ent/securitysecret/securitysecret.go
Normal file
86
backend/ent/securitysecret/securitysecret.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// Code generated by ent, DO NOT EDIT.
|
||||||
|
|
||||||
|
package securitysecret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"entgo.io/ent/dialect/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Label holds the string label denoting the securitysecret type in the database.
|
||||||
|
Label = "security_secret"
|
||||||
|
// FieldID holds the string denoting the id field in the database.
|
||||||
|
FieldID = "id"
|
||||||
|
// FieldCreatedAt holds the string denoting the created_at field in the database.
|
||||||
|
FieldCreatedAt = "created_at"
|
||||||
|
// FieldUpdatedAt holds the string denoting the updated_at field in the database.
|
||||||
|
FieldUpdatedAt = "updated_at"
|
||||||
|
// FieldKey holds the string denoting the key field in the database.
|
||||||
|
FieldKey = "key"
|
||||||
|
// FieldValue holds the string denoting the value field in the database.
|
||||||
|
FieldValue = "value"
|
||||||
|
// Table holds the table name of the securitysecret in the database.
|
||||||
|
Table = "security_secrets"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Columns holds all SQL columns for securitysecret fields.
|
||||||
|
var Columns = []string{
|
||||||
|
FieldID,
|
||||||
|
FieldCreatedAt,
|
||||||
|
FieldUpdatedAt,
|
||||||
|
FieldKey,
|
||||||
|
FieldValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidColumn reports if the column name is valid (part of the table columns).
|
||||||
|
func ValidColumn(column string) bool {
|
||||||
|
for i := range Columns {
|
||||||
|
if column == Columns[i] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
|
||||||
|
DefaultCreatedAt func() time.Time
|
||||||
|
// DefaultUpdatedAt holds the default value on creation for the "updated_at" field.
|
||||||
|
DefaultUpdatedAt func() time.Time
|
||||||
|
// UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field.
|
||||||
|
UpdateDefaultUpdatedAt func() time.Time
|
||||||
|
// KeyValidator is a validator for the "key" field. It is called by the builders before save.
|
||||||
|
KeyValidator func(string) error
|
||||||
|
// ValueValidator is a validator for the "value" field. It is called by the builders before save.
|
||||||
|
ValueValidator func(string) error
|
||||||
|
)
|
||||||
|
|
||||||
|
// OrderOption defines the ordering options for the SecuritySecret queries.
|
||||||
|
type OrderOption func(*sql.Selector)
|
||||||
|
|
||||||
|
// ByID orders the results by the id field.
|
||||||
|
func ByID(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldID, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByCreatedAt orders the results by the created_at field.
|
||||||
|
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByUpdatedAt orders the results by the updated_at field.
|
||||||
|
func ByUpdatedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldUpdatedAt, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByKey orders the results by the key field.
|
||||||
|
func ByKey(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldKey, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByValue orders the results by the value field.
|
||||||
|
func ByValue(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldValue, opts...).ToFunc()
|
||||||
|
}
|
||||||
300
backend/ent/securitysecret/where.go
Normal file
300
backend/ent/securitysecret/where.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
// Code generated by ent, DO NOT EDIT.
|
||||||
|
|
||||||
|
package securitysecret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"entgo.io/ent/dialect/sql"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/predicate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ID filters vertices based on their ID field.
|
||||||
|
func ID(id int64) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEQ(FieldID, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDEQ applies the EQ predicate on the ID field.
|
||||||
|
func IDEQ(id int64) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEQ(FieldID, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDNEQ applies the NEQ predicate on the ID field.
|
||||||
|
func IDNEQ(id int64) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldNEQ(FieldID, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDIn applies the In predicate on the ID field.
|
||||||
|
func IDIn(ids ...int64) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldIn(FieldID, ids...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDNotIn applies the NotIn predicate on the ID field.
|
||||||
|
func IDNotIn(ids ...int64) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldNotIn(FieldID, ids...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDGT applies the GT predicate on the ID field.
|
||||||
|
func IDGT(id int64) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldGT(FieldID, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDGTE applies the GTE predicate on the ID field.
|
||||||
|
func IDGTE(id int64) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldGTE(FieldID, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDLT applies the LT predicate on the ID field.
|
||||||
|
func IDLT(id int64) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldLT(FieldID, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDLTE applies the LTE predicate on the ID field.
|
||||||
|
func IDLTE(id int64) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldLTE(FieldID, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
|
||||||
|
func CreatedAt(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEQ(FieldCreatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatedAt applies equality check predicate on the "updated_at" field. It's identical to UpdatedAtEQ.
|
||||||
|
func UpdatedAt(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEQ(FieldUpdatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key applies equality check predicate on the "key" field. It's identical to KeyEQ.
|
||||||
|
func Key(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEQ(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value applies equality check predicate on the "value" field. It's identical to ValueEQ.
|
||||||
|
func Value(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEQ(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
||||||
|
func CreatedAtEQ(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEQ(FieldCreatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAtNEQ applies the NEQ predicate on the "created_at" field.
|
||||||
|
func CreatedAtNEQ(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldNEQ(FieldCreatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAtIn applies the In predicate on the "created_at" field.
|
||||||
|
func CreatedAtIn(vs ...time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldIn(FieldCreatedAt, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAtNotIn applies the NotIn predicate on the "created_at" field.
|
||||||
|
func CreatedAtNotIn(vs ...time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldNotIn(FieldCreatedAt, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAtGT applies the GT predicate on the "created_at" field.
|
||||||
|
func CreatedAtGT(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldGT(FieldCreatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAtGTE applies the GTE predicate on the "created_at" field.
|
||||||
|
func CreatedAtGTE(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldGTE(FieldCreatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAtLT applies the LT predicate on the "created_at" field.
|
||||||
|
func CreatedAtLT(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldLT(FieldCreatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAtLTE applies the LTE predicate on the "created_at" field.
|
||||||
|
func CreatedAtLTE(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldLTE(FieldCreatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatedAtEQ applies the EQ predicate on the "updated_at" field.
|
||||||
|
func UpdatedAtEQ(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEQ(FieldUpdatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatedAtNEQ applies the NEQ predicate on the "updated_at" field.
|
||||||
|
func UpdatedAtNEQ(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldNEQ(FieldUpdatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatedAtIn applies the In predicate on the "updated_at" field.
|
||||||
|
func UpdatedAtIn(vs ...time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldIn(FieldUpdatedAt, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatedAtNotIn applies the NotIn predicate on the "updated_at" field.
|
||||||
|
func UpdatedAtNotIn(vs ...time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldNotIn(FieldUpdatedAt, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatedAtGT applies the GT predicate on the "updated_at" field.
|
||||||
|
func UpdatedAtGT(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldGT(FieldUpdatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatedAtGTE applies the GTE predicate on the "updated_at" field.
|
||||||
|
func UpdatedAtGTE(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldGTE(FieldUpdatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatedAtLT applies the LT predicate on the "updated_at" field.
|
||||||
|
func UpdatedAtLT(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldLT(FieldUpdatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatedAtLTE applies the LTE predicate on the "updated_at" field.
|
||||||
|
func UpdatedAtLTE(v time.Time) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldLTE(FieldUpdatedAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyEQ applies the EQ predicate on the "key" field.
|
||||||
|
func KeyEQ(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEQ(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyNEQ applies the NEQ predicate on the "key" field.
|
||||||
|
func KeyNEQ(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldNEQ(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyIn applies the In predicate on the "key" field.
|
||||||
|
func KeyIn(vs ...string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldIn(FieldKey, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyNotIn applies the NotIn predicate on the "key" field.
|
||||||
|
func KeyNotIn(vs ...string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldNotIn(FieldKey, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyGT applies the GT predicate on the "key" field.
|
||||||
|
func KeyGT(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldGT(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyGTE applies the GTE predicate on the "key" field.
|
||||||
|
func KeyGTE(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldGTE(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyLT applies the LT predicate on the "key" field.
|
||||||
|
func KeyLT(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldLT(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyLTE applies the LTE predicate on the "key" field.
|
||||||
|
func KeyLTE(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldLTE(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyContains applies the Contains predicate on the "key" field.
|
||||||
|
func KeyContains(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldContains(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyHasPrefix applies the HasPrefix predicate on the "key" field.
|
||||||
|
func KeyHasPrefix(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldHasPrefix(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyHasSuffix applies the HasSuffix predicate on the "key" field.
|
||||||
|
func KeyHasSuffix(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldHasSuffix(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyEqualFold applies the EqualFold predicate on the "key" field.
|
||||||
|
func KeyEqualFold(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEqualFold(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyContainsFold applies the ContainsFold predicate on the "key" field.
|
||||||
|
func KeyContainsFold(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldContainsFold(FieldKey, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueEQ applies the EQ predicate on the "value" field.
|
||||||
|
func ValueEQ(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEQ(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueNEQ applies the NEQ predicate on the "value" field.
|
||||||
|
func ValueNEQ(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldNEQ(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueIn applies the In predicate on the "value" field.
|
||||||
|
func ValueIn(vs ...string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldIn(FieldValue, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueNotIn applies the NotIn predicate on the "value" field.
|
||||||
|
func ValueNotIn(vs ...string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldNotIn(FieldValue, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueGT applies the GT predicate on the "value" field.
|
||||||
|
func ValueGT(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldGT(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueGTE applies the GTE predicate on the "value" field.
|
||||||
|
func ValueGTE(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldGTE(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueLT applies the LT predicate on the "value" field.
|
||||||
|
func ValueLT(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldLT(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueLTE applies the LTE predicate on the "value" field.
|
||||||
|
func ValueLTE(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldLTE(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueContains applies the Contains predicate on the "value" field.
|
||||||
|
func ValueContains(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldContains(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueHasPrefix applies the HasPrefix predicate on the "value" field.
|
||||||
|
func ValueHasPrefix(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldHasPrefix(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueHasSuffix applies the HasSuffix predicate on the "value" field.
|
||||||
|
func ValueHasSuffix(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldHasSuffix(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueEqualFold applies the EqualFold predicate on the "value" field.
|
||||||
|
func ValueEqualFold(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldEqualFold(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueContainsFold applies the ContainsFold predicate on the "value" field.
|
||||||
|
func ValueContainsFold(v string) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.FieldContainsFold(FieldValue, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// And groups predicates with the AND operator between them.
|
||||||
|
func And(predicates ...predicate.SecuritySecret) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.AndPredicates(predicates...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or groups predicates with the OR operator between them.
|
||||||
|
func Or(predicates ...predicate.SecuritySecret) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.OrPredicates(predicates...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not applies the not operator on the given predicate.
|
||||||
|
func Not(p predicate.SecuritySecret) predicate.SecuritySecret {
|
||||||
|
return predicate.SecuritySecret(sql.NotPredicates(p))
|
||||||
|
}
|
||||||
626
backend/ent/securitysecret_create.go
Normal file
626
backend/ent/securitysecret_create.go
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
// Code generated by ent, DO NOT EDIT.
|
||||||
|
|
||||||
|
package ent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"entgo.io/ent/dialect/sql"
|
||||||
|
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||||
|
"entgo.io/ent/schema/field"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecuritySecretCreate is the builder for creating a SecuritySecret entity.
|
||||||
|
type SecuritySecretCreate struct {
|
||||||
|
config
|
||||||
|
mutation *SecuritySecretMutation
|
||||||
|
hooks []Hook
|
||||||
|
conflict []sql.ConflictOption
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCreatedAt sets the "created_at" field.
|
||||||
|
func (_c *SecuritySecretCreate) SetCreatedAt(v time.Time) *SecuritySecretCreate {
|
||||||
|
_c.mutation.SetCreatedAt(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableCreatedAt sets the "created_at" field if the given value is not nil.
|
||||||
|
func (_c *SecuritySecretCreate) SetNillableCreatedAt(v *time.Time) *SecuritySecretCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetCreatedAt(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUpdatedAt sets the "updated_at" field.
|
||||||
|
func (_c *SecuritySecretCreate) SetUpdatedAt(v time.Time) *SecuritySecretCreate {
|
||||||
|
_c.mutation.SetUpdatedAt(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableUpdatedAt sets the "updated_at" field if the given value is not nil.
|
||||||
|
func (_c *SecuritySecretCreate) SetNillableUpdatedAt(v *time.Time) *SecuritySecretCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetUpdatedAt(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKey sets the "key" field.
|
||||||
|
func (_c *SecuritySecretCreate) SetKey(v string) *SecuritySecretCreate {
|
||||||
|
_c.mutation.SetKey(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValue sets the "value" field.
|
||||||
|
func (_c *SecuritySecretCreate) SetValue(v string) *SecuritySecretCreate {
|
||||||
|
_c.mutation.SetValue(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutation returns the SecuritySecretMutation object of the builder.
|
||||||
|
func (_c *SecuritySecretCreate) Mutation() *SecuritySecretMutation {
|
||||||
|
return _c.mutation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save creates the SecuritySecret in the database.
|
||||||
|
func (_c *SecuritySecretCreate) Save(ctx context.Context) (*SecuritySecret, error) {
|
||||||
|
_c.defaults()
|
||||||
|
return withHooks(ctx, _c.sqlSave, _c.mutation, _c.hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveX calls Save and panics if Save returns an error.
|
||||||
|
func (_c *SecuritySecretCreate) SaveX(ctx context.Context) *SecuritySecret {
|
||||||
|
v, err := _c.Save(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes the query.
|
||||||
|
func (_c *SecuritySecretCreate) Exec(ctx context.Context) error {
|
||||||
|
_, err := _c.Save(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
|
func (_c *SecuritySecretCreate) ExecX(ctx context.Context) {
|
||||||
|
if err := _c.Exec(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaults sets the default values of the builder before save.
|
||||||
|
func (_c *SecuritySecretCreate) defaults() {
|
||||||
|
if _, ok := _c.mutation.CreatedAt(); !ok {
|
||||||
|
v := securitysecret.DefaultCreatedAt()
|
||||||
|
_c.mutation.SetCreatedAt(v)
|
||||||
|
}
|
||||||
|
if _, ok := _c.mutation.UpdatedAt(); !ok {
|
||||||
|
v := securitysecret.DefaultUpdatedAt()
|
||||||
|
_c.mutation.SetUpdatedAt(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check runs all checks and user-defined validators on the builder.
|
||||||
|
func (_c *SecuritySecretCreate) check() error {
|
||||||
|
if _, ok := _c.mutation.CreatedAt(); !ok {
|
||||||
|
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "SecuritySecret.created_at"`)}
|
||||||
|
}
|
||||||
|
if _, ok := _c.mutation.UpdatedAt(); !ok {
|
||||||
|
return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "SecuritySecret.updated_at"`)}
|
||||||
|
}
|
||||||
|
if _, ok := _c.mutation.Key(); !ok {
|
||||||
|
return &ValidationError{Name: "key", err: errors.New(`ent: missing required field "SecuritySecret.key"`)}
|
||||||
|
}
|
||||||
|
if v, ok := _c.mutation.Key(); ok {
|
||||||
|
if err := securitysecret.KeyValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "SecuritySecret.key": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := _c.mutation.Value(); !ok {
|
||||||
|
return &ValidationError{Name: "value", err: errors.New(`ent: missing required field "SecuritySecret.value"`)}
|
||||||
|
}
|
||||||
|
if v, ok := _c.mutation.Value(); ok {
|
||||||
|
if err := securitysecret.ValueValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "value", err: fmt.Errorf(`ent: validator failed for field "SecuritySecret.value": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *SecuritySecretCreate) sqlSave(ctx context.Context) (*SecuritySecret, error) {
|
||||||
|
if err := _c.check(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_node, _spec := _c.createSpec()
|
||||||
|
if err := sqlgraph.CreateNode(ctx, _c.driver, _spec); err != nil {
|
||||||
|
if sqlgraph.IsConstraintError(err) {
|
||||||
|
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
id := _spec.ID.Value.(int64)
|
||||||
|
_node.ID = int64(id)
|
||||||
|
_c.mutation.id = &_node.ID
|
||||||
|
_c.mutation.done = true
|
||||||
|
return _node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *SecuritySecretCreate) createSpec() (*SecuritySecret, *sqlgraph.CreateSpec) {
|
||||||
|
var (
|
||||||
|
_node = &SecuritySecret{config: _c.config}
|
||||||
|
_spec = sqlgraph.NewCreateSpec(securitysecret.Table, sqlgraph.NewFieldSpec(securitysecret.FieldID, field.TypeInt64))
|
||||||
|
)
|
||||||
|
_spec.OnConflict = _c.conflict
|
||||||
|
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||||
|
_spec.SetField(securitysecret.FieldCreatedAt, field.TypeTime, value)
|
||||||
|
_node.CreatedAt = value
|
||||||
|
}
|
||||||
|
if value, ok := _c.mutation.UpdatedAt(); ok {
|
||||||
|
_spec.SetField(securitysecret.FieldUpdatedAt, field.TypeTime, value)
|
||||||
|
_node.UpdatedAt = value
|
||||||
|
}
|
||||||
|
if value, ok := _c.mutation.Key(); ok {
|
||||||
|
_spec.SetField(securitysecret.FieldKey, field.TypeString, value)
|
||||||
|
_node.Key = value
|
||||||
|
}
|
||||||
|
if value, ok := _c.mutation.Value(); ok {
|
||||||
|
_spec.SetField(securitysecret.FieldValue, field.TypeString, value)
|
||||||
|
_node.Value = value
|
||||||
|
}
|
||||||
|
return _node, _spec
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
|
||||||
|
// of the `INSERT` statement. For example:
|
||||||
|
//
|
||||||
|
// client.SecuritySecret.Create().
|
||||||
|
// SetCreatedAt(v).
|
||||||
|
// OnConflict(
|
||||||
|
// // Update the row with the new values
|
||||||
|
// // the was proposed for insertion.
|
||||||
|
// sql.ResolveWithNewValues(),
|
||||||
|
// ).
|
||||||
|
// // Override some of the fields with custom
|
||||||
|
// // update values.
|
||||||
|
// Update(func(u *ent.SecuritySecretUpsert) {
|
||||||
|
// SetCreatedAt(v+v).
|
||||||
|
// }).
|
||||||
|
// Exec(ctx)
|
||||||
|
func (_c *SecuritySecretCreate) OnConflict(opts ...sql.ConflictOption) *SecuritySecretUpsertOne {
|
||||||
|
_c.conflict = opts
|
||||||
|
return &SecuritySecretUpsertOne{
|
||||||
|
create: _c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnConflictColumns calls `OnConflict` and configures the columns
|
||||||
|
// as conflict target. Using this option is equivalent to using:
|
||||||
|
//
|
||||||
|
// client.SecuritySecret.Create().
|
||||||
|
// OnConflict(sql.ConflictColumns(columns...)).
|
||||||
|
// Exec(ctx)
|
||||||
|
func (_c *SecuritySecretCreate) OnConflictColumns(columns ...string) *SecuritySecretUpsertOne {
|
||||||
|
_c.conflict = append(_c.conflict, sql.ConflictColumns(columns...))
|
||||||
|
return &SecuritySecretUpsertOne{
|
||||||
|
create: _c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
// SecuritySecretUpsertOne is the builder for "upsert"-ing
|
||||||
|
// one SecuritySecret node.
|
||||||
|
SecuritySecretUpsertOne struct {
|
||||||
|
create *SecuritySecretCreate
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecuritySecretUpsert is the "OnConflict" setter.
|
||||||
|
SecuritySecretUpsert struct {
|
||||||
|
*sql.UpdateSet
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetUpdatedAt sets the "updated_at" field.
|
||||||
|
func (u *SecuritySecretUpsert) SetUpdatedAt(v time.Time) *SecuritySecretUpsert {
|
||||||
|
u.Set(securitysecret.FieldUpdatedAt, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create.
|
||||||
|
func (u *SecuritySecretUpsert) UpdateUpdatedAt() *SecuritySecretUpsert {
|
||||||
|
u.SetExcluded(securitysecret.FieldUpdatedAt)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKey sets the "key" field.
|
||||||
|
func (u *SecuritySecretUpsert) SetKey(v string) *SecuritySecretUpsert {
|
||||||
|
u.Set(securitysecret.FieldKey, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateKey sets the "key" field to the value that was provided on create.
|
||||||
|
func (u *SecuritySecretUpsert) UpdateKey() *SecuritySecretUpsert {
|
||||||
|
u.SetExcluded(securitysecret.FieldKey)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValue sets the "value" field.
|
||||||
|
func (u *SecuritySecretUpsert) SetValue(v string) *SecuritySecretUpsert {
|
||||||
|
u.Set(securitysecret.FieldValue, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateValue sets the "value" field to the value that was provided on create.
|
||||||
|
func (u *SecuritySecretUpsert) UpdateValue() *SecuritySecretUpsert {
|
||||||
|
u.SetExcluded(securitysecret.FieldValue)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateNewValues updates the mutable fields using the new values that were set on create.
|
||||||
|
// Using this option is equivalent to using:
|
||||||
|
//
|
||||||
|
// client.SecuritySecret.Create().
|
||||||
|
// OnConflict(
|
||||||
|
// sql.ResolveWithNewValues(),
|
||||||
|
// ).
|
||||||
|
// Exec(ctx)
|
||||||
|
func (u *SecuritySecretUpsertOne) UpdateNewValues() *SecuritySecretUpsertOne {
|
||||||
|
u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues())
|
||||||
|
u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) {
|
||||||
|
if _, exists := u.create.mutation.CreatedAt(); exists {
|
||||||
|
s.SetIgnore(securitysecret.FieldCreatedAt)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore sets each column to itself in case of conflict.
|
||||||
|
// Using this option is equivalent to using:
|
||||||
|
//
|
||||||
|
// client.SecuritySecret.Create().
|
||||||
|
// OnConflict(sql.ResolveWithIgnore()).
|
||||||
|
// Exec(ctx)
|
||||||
|
func (u *SecuritySecretUpsertOne) Ignore() *SecuritySecretUpsertOne {
|
||||||
|
u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore())
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoNothing configures the conflict_action to `DO NOTHING`.
|
||||||
|
// Supported only by SQLite and PostgreSQL.
|
||||||
|
func (u *SecuritySecretUpsertOne) DoNothing() *SecuritySecretUpsertOne {
|
||||||
|
u.create.conflict = append(u.create.conflict, sql.DoNothing())
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update allows overriding fields `UPDATE` values. See the SecuritySecretCreate.OnConflict
|
||||||
|
// documentation for more info.
|
||||||
|
func (u *SecuritySecretUpsertOne) Update(set func(*SecuritySecretUpsert)) *SecuritySecretUpsertOne {
|
||||||
|
u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) {
|
||||||
|
set(&SecuritySecretUpsert{UpdateSet: update})
|
||||||
|
}))
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUpdatedAt sets the "updated_at" field.
|
||||||
|
func (u *SecuritySecretUpsertOne) SetUpdatedAt(v time.Time) *SecuritySecretUpsertOne {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.SetUpdatedAt(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create.
|
||||||
|
func (u *SecuritySecretUpsertOne) UpdateUpdatedAt() *SecuritySecretUpsertOne {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.UpdateUpdatedAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKey sets the "key" field.
|
||||||
|
func (u *SecuritySecretUpsertOne) SetKey(v string) *SecuritySecretUpsertOne {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.SetKey(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateKey sets the "key" field to the value that was provided on create.
|
||||||
|
func (u *SecuritySecretUpsertOne) UpdateKey() *SecuritySecretUpsertOne {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.UpdateKey()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValue sets the "value" field.
|
||||||
|
func (u *SecuritySecretUpsertOne) SetValue(v string) *SecuritySecretUpsertOne {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.SetValue(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateValue sets the "value" field to the value that was provided on create.
|
||||||
|
func (u *SecuritySecretUpsertOne) UpdateValue() *SecuritySecretUpsertOne {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.UpdateValue()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes the query.
|
||||||
|
func (u *SecuritySecretUpsertOne) Exec(ctx context.Context) error {
|
||||||
|
if len(u.create.conflict) == 0 {
|
||||||
|
return errors.New("ent: missing options for SecuritySecretCreate.OnConflict")
|
||||||
|
}
|
||||||
|
return u.create.Exec(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
|
func (u *SecuritySecretUpsertOne) ExecX(ctx context.Context) {
|
||||||
|
if err := u.create.Exec(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes the UPSERT query and returns the inserted/updated ID.
|
||||||
|
func (u *SecuritySecretUpsertOne) ID(ctx context.Context) (id int64, err error) {
|
||||||
|
node, err := u.create.Save(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
return node.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDX is like ID, but panics if an error occurs.
|
||||||
|
func (u *SecuritySecretUpsertOne) IDX(ctx context.Context) int64 {
|
||||||
|
id, err := u.ID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecuritySecretCreateBulk is the builder for creating many SecuritySecret entities in bulk.
|
||||||
|
type SecuritySecretCreateBulk struct {
|
||||||
|
config
|
||||||
|
err error
|
||||||
|
builders []*SecuritySecretCreate
|
||||||
|
conflict []sql.ConflictOption
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save creates the SecuritySecret entities in the database.
|
||||||
|
func (_c *SecuritySecretCreateBulk) Save(ctx context.Context) ([]*SecuritySecret, error) {
|
||||||
|
if _c.err != nil {
|
||||||
|
return nil, _c.err
|
||||||
|
}
|
||||||
|
specs := make([]*sqlgraph.CreateSpec, len(_c.builders))
|
||||||
|
nodes := make([]*SecuritySecret, len(_c.builders))
|
||||||
|
mutators := make([]Mutator, len(_c.builders))
|
||||||
|
for i := range _c.builders {
|
||||||
|
func(i int, root context.Context) {
|
||||||
|
builder := _c.builders[i]
|
||||||
|
builder.defaults()
|
||||||
|
var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) {
|
||||||
|
mutation, ok := m.(*SecuritySecretMutation)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unexpected mutation type %T", m)
|
||||||
|
}
|
||||||
|
if err := builder.check(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
builder.mutation = mutation
|
||||||
|
var err error
|
||||||
|
nodes[i], specs[i] = builder.createSpec()
|
||||||
|
if i < len(mutators)-1 {
|
||||||
|
_, err = mutators[i+1].Mutate(root, _c.builders[i+1].mutation)
|
||||||
|
} else {
|
||||||
|
spec := &sqlgraph.BatchCreateSpec{Nodes: specs}
|
||||||
|
spec.OnConflict = _c.conflict
|
||||||
|
// Invoke the actual operation on the latest mutation in the chain.
|
||||||
|
if err = sqlgraph.BatchCreate(ctx, _c.driver, spec); err != nil {
|
||||||
|
if sqlgraph.IsConstraintError(err) {
|
||||||
|
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mutation.id = &nodes[i].ID
|
||||||
|
if specs[i].ID.Value != nil {
|
||||||
|
id := specs[i].ID.Value.(int64)
|
||||||
|
nodes[i].ID = int64(id)
|
||||||
|
}
|
||||||
|
mutation.done = true
|
||||||
|
return nodes[i], nil
|
||||||
|
})
|
||||||
|
for i := len(builder.hooks) - 1; i >= 0; i-- {
|
||||||
|
mut = builder.hooks[i](mut)
|
||||||
|
}
|
||||||
|
mutators[i] = mut
|
||||||
|
}(i, ctx)
|
||||||
|
}
|
||||||
|
if len(mutators) > 0 {
|
||||||
|
if _, err := mutators[0].Mutate(ctx, _c.builders[0].mutation); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveX is like Save, but panics if an error occurs.
|
||||||
|
func (_c *SecuritySecretCreateBulk) SaveX(ctx context.Context) []*SecuritySecret {
|
||||||
|
v, err := _c.Save(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes the query.
|
||||||
|
func (_c *SecuritySecretCreateBulk) Exec(ctx context.Context) error {
|
||||||
|
_, err := _c.Save(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
|
func (_c *SecuritySecretCreateBulk) ExecX(ctx context.Context) {
|
||||||
|
if err := _c.Exec(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
|
||||||
|
// of the `INSERT` statement. For example:
|
||||||
|
//
|
||||||
|
// client.SecuritySecret.CreateBulk(builders...).
|
||||||
|
// OnConflict(
|
||||||
|
// // Update the row with the new values
|
||||||
|
// // the was proposed for insertion.
|
||||||
|
// sql.ResolveWithNewValues(),
|
||||||
|
// ).
|
||||||
|
// // Override some of the fields with custom
|
||||||
|
// // update values.
|
||||||
|
// Update(func(u *ent.SecuritySecretUpsert) {
|
||||||
|
// SetCreatedAt(v+v).
|
||||||
|
// }).
|
||||||
|
// Exec(ctx)
|
||||||
|
func (_c *SecuritySecretCreateBulk) OnConflict(opts ...sql.ConflictOption) *SecuritySecretUpsertBulk {
|
||||||
|
_c.conflict = opts
|
||||||
|
return &SecuritySecretUpsertBulk{
|
||||||
|
create: _c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnConflictColumns calls `OnConflict` and configures the columns
|
||||||
|
// as conflict target. Using this option is equivalent to using:
|
||||||
|
//
|
||||||
|
// client.SecuritySecret.Create().
|
||||||
|
// OnConflict(sql.ConflictColumns(columns...)).
|
||||||
|
// Exec(ctx)
|
||||||
|
func (_c *SecuritySecretCreateBulk) OnConflictColumns(columns ...string) *SecuritySecretUpsertBulk {
|
||||||
|
_c.conflict = append(_c.conflict, sql.ConflictColumns(columns...))
|
||||||
|
return &SecuritySecretUpsertBulk{
|
||||||
|
create: _c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecuritySecretUpsertBulk is the builder for "upsert"-ing
|
||||||
|
// a bulk of SecuritySecret nodes.
|
||||||
|
type SecuritySecretUpsertBulk struct {
|
||||||
|
create *SecuritySecretCreateBulk
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateNewValues updates the mutable fields using the new values that
|
||||||
|
// were set on create. Using this option is equivalent to using:
|
||||||
|
//
|
||||||
|
// client.SecuritySecret.Create().
|
||||||
|
// OnConflict(
|
||||||
|
// sql.ResolveWithNewValues(),
|
||||||
|
// ).
|
||||||
|
// Exec(ctx)
|
||||||
|
func (u *SecuritySecretUpsertBulk) UpdateNewValues() *SecuritySecretUpsertBulk {
|
||||||
|
u.create.conflict = append(u.create.conflict, sql.ResolveWithNewValues())
|
||||||
|
u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(s *sql.UpdateSet) {
|
||||||
|
for _, b := range u.create.builders {
|
||||||
|
if _, exists := b.mutation.CreatedAt(); exists {
|
||||||
|
s.SetIgnore(securitysecret.FieldCreatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore sets each column to itself in case of conflict.
|
||||||
|
// Using this option is equivalent to using:
|
||||||
|
//
|
||||||
|
// client.SecuritySecret.Create().
|
||||||
|
// OnConflict(sql.ResolveWithIgnore()).
|
||||||
|
// Exec(ctx)
|
||||||
|
func (u *SecuritySecretUpsertBulk) Ignore() *SecuritySecretUpsertBulk {
|
||||||
|
u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore())
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoNothing configures the conflict_action to `DO NOTHING`.
|
||||||
|
// Supported only by SQLite and PostgreSQL.
|
||||||
|
func (u *SecuritySecretUpsertBulk) DoNothing() *SecuritySecretUpsertBulk {
|
||||||
|
u.create.conflict = append(u.create.conflict, sql.DoNothing())
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update allows overriding fields `UPDATE` values. See the SecuritySecretCreateBulk.OnConflict
|
||||||
|
// documentation for more info.
|
||||||
|
func (u *SecuritySecretUpsertBulk) Update(set func(*SecuritySecretUpsert)) *SecuritySecretUpsertBulk {
|
||||||
|
u.create.conflict = append(u.create.conflict, sql.ResolveWith(func(update *sql.UpdateSet) {
|
||||||
|
set(&SecuritySecretUpsert{UpdateSet: update})
|
||||||
|
}))
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUpdatedAt sets the "updated_at" field.
|
||||||
|
func (u *SecuritySecretUpsertBulk) SetUpdatedAt(v time.Time) *SecuritySecretUpsertBulk {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.SetUpdatedAt(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUpdatedAt sets the "updated_at" field to the value that was provided on create.
|
||||||
|
func (u *SecuritySecretUpsertBulk) UpdateUpdatedAt() *SecuritySecretUpsertBulk {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.UpdateUpdatedAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKey sets the "key" field.
|
||||||
|
func (u *SecuritySecretUpsertBulk) SetKey(v string) *SecuritySecretUpsertBulk {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.SetKey(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateKey sets the "key" field to the value that was provided on create.
|
||||||
|
func (u *SecuritySecretUpsertBulk) UpdateKey() *SecuritySecretUpsertBulk {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.UpdateKey()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValue sets the "value" field.
|
||||||
|
func (u *SecuritySecretUpsertBulk) SetValue(v string) *SecuritySecretUpsertBulk {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.SetValue(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateValue sets the "value" field to the value that was provided on create.
|
||||||
|
func (u *SecuritySecretUpsertBulk) UpdateValue() *SecuritySecretUpsertBulk {
|
||||||
|
return u.Update(func(s *SecuritySecretUpsert) {
|
||||||
|
s.UpdateValue()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes the query.
|
||||||
|
func (u *SecuritySecretUpsertBulk) Exec(ctx context.Context) error {
|
||||||
|
if u.create.err != nil {
|
||||||
|
return u.create.err
|
||||||
|
}
|
||||||
|
for i, b := range u.create.builders {
|
||||||
|
if len(b.conflict) != 0 {
|
||||||
|
return fmt.Errorf("ent: OnConflict was set for builder %d. Set it on the SecuritySecretCreateBulk instead", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(u.create.conflict) == 0 {
|
||||||
|
return errors.New("ent: missing options for SecuritySecretCreateBulk.OnConflict")
|
||||||
|
}
|
||||||
|
return u.create.Exec(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
|
func (u *SecuritySecretUpsertBulk) ExecX(ctx context.Context) {
|
||||||
|
if err := u.create.Exec(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
88
backend/ent/securitysecret_delete.go
Normal file
88
backend/ent/securitysecret_delete.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// Code generated by ent, DO NOT EDIT.
|
||||||
|
|
||||||
|
package ent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"entgo.io/ent/dialect/sql"
|
||||||
|
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||||
|
"entgo.io/ent/schema/field"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/predicate"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecuritySecretDelete is the builder for deleting a SecuritySecret entity.
|
||||||
|
type SecuritySecretDelete struct {
|
||||||
|
config
|
||||||
|
hooks []Hook
|
||||||
|
mutation *SecuritySecretMutation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where appends a list predicates to the SecuritySecretDelete builder.
|
||||||
|
func (_d *SecuritySecretDelete) Where(ps ...predicate.SecuritySecret) *SecuritySecretDelete {
|
||||||
|
_d.mutation.Where(ps...)
|
||||||
|
return _d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes the deletion query and returns how many vertices were deleted.
|
||||||
|
func (_d *SecuritySecretDelete) Exec(ctx context.Context) (int, error) {
|
||||||
|
return withHooks(ctx, _d.sqlExec, _d.mutation, _d.hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
|
func (_d *SecuritySecretDelete) ExecX(ctx context.Context) int {
|
||||||
|
n, err := _d.Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_d *SecuritySecretDelete) sqlExec(ctx context.Context) (int, error) {
|
||||||
|
_spec := sqlgraph.NewDeleteSpec(securitysecret.Table, sqlgraph.NewFieldSpec(securitysecret.FieldID, field.TypeInt64))
|
||||||
|
if ps := _d.mutation.predicates; len(ps) > 0 {
|
||||||
|
_spec.Predicate = func(selector *sql.Selector) {
|
||||||
|
for i := range ps {
|
||||||
|
ps[i](selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
affected, err := sqlgraph.DeleteNodes(ctx, _d.driver, _spec)
|
||||||
|
if err != nil && sqlgraph.IsConstraintError(err) {
|
||||||
|
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||||
|
}
|
||||||
|
_d.mutation.done = true
|
||||||
|
return affected, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecuritySecretDeleteOne is the builder for deleting a single SecuritySecret entity.
|
||||||
|
type SecuritySecretDeleteOne struct {
|
||||||
|
_d *SecuritySecretDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where appends a list predicates to the SecuritySecretDelete builder.
|
||||||
|
func (_d *SecuritySecretDeleteOne) Where(ps ...predicate.SecuritySecret) *SecuritySecretDeleteOne {
|
||||||
|
_d._d.mutation.Where(ps...)
|
||||||
|
return _d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes the deletion query.
|
||||||
|
func (_d *SecuritySecretDeleteOne) Exec(ctx context.Context) error {
|
||||||
|
n, err := _d._d.Exec(ctx)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
return err
|
||||||
|
case n == 0:
|
||||||
|
return &NotFoundError{securitysecret.Label}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
|
func (_d *SecuritySecretDeleteOne) ExecX(ctx context.Context) {
|
||||||
|
if err := _d.Exec(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
564
backend/ent/securitysecret_query.go
Normal file
564
backend/ent/securitysecret_query.go
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
// Code generated by ent, DO NOT EDIT.
|
||||||
|
|
||||||
|
package ent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"entgo.io/ent"
|
||||||
|
"entgo.io/ent/dialect"
|
||||||
|
"entgo.io/ent/dialect/sql"
|
||||||
|
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||||
|
"entgo.io/ent/schema/field"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/predicate"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecuritySecretQuery is the builder for querying SecuritySecret entities.
|
||||||
|
type SecuritySecretQuery struct {
|
||||||
|
config
|
||||||
|
ctx *QueryContext
|
||||||
|
order []securitysecret.OrderOption
|
||||||
|
inters []Interceptor
|
||||||
|
predicates []predicate.SecuritySecret
|
||||||
|
modifiers []func(*sql.Selector)
|
||||||
|
// intermediate query (i.e. traversal path).
|
||||||
|
sql *sql.Selector
|
||||||
|
path func(context.Context) (*sql.Selector, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where adds a new predicate for the SecuritySecretQuery builder.
|
||||||
|
func (_q *SecuritySecretQuery) Where(ps ...predicate.SecuritySecret) *SecuritySecretQuery {
|
||||||
|
_q.predicates = append(_q.predicates, ps...)
|
||||||
|
return _q
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit the number of records to be returned by this query.
|
||||||
|
func (_q *SecuritySecretQuery) Limit(limit int) *SecuritySecretQuery {
|
||||||
|
_q.ctx.Limit = &limit
|
||||||
|
return _q
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset to start from.
|
||||||
|
func (_q *SecuritySecretQuery) Offset(offset int) *SecuritySecretQuery {
|
||||||
|
_q.ctx.Offset = &offset
|
||||||
|
return _q
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unique configures the query builder to filter duplicate records on query.
|
||||||
|
// By default, unique is set to true, and can be disabled using this method.
|
||||||
|
func (_q *SecuritySecretQuery) Unique(unique bool) *SecuritySecretQuery {
|
||||||
|
_q.ctx.Unique = &unique
|
||||||
|
return _q
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order specifies how the records should be ordered.
|
||||||
|
func (_q *SecuritySecretQuery) Order(o ...securitysecret.OrderOption) *SecuritySecretQuery {
|
||||||
|
_q.order = append(_q.order, o...)
|
||||||
|
return _q
|
||||||
|
}
|
||||||
|
|
||||||
|
// First returns the first SecuritySecret entity from the query.
|
||||||
|
// Returns a *NotFoundError when no SecuritySecret was found.
|
||||||
|
func (_q *SecuritySecretQuery) First(ctx context.Context) (*SecuritySecret, error) {
|
||||||
|
nodes, err := _q.Limit(1).All(setContextOp(ctx, _q.ctx, ent.OpQueryFirst))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
return nil, &NotFoundError{securitysecret.Label}
|
||||||
|
}
|
||||||
|
return nodes[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstX is like First, but panics if an error occurs.
|
||||||
|
func (_q *SecuritySecretQuery) FirstX(ctx context.Context) *SecuritySecret {
|
||||||
|
node, err := _q.First(ctx)
|
||||||
|
if err != nil && !IsNotFound(err) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstID returns the first SecuritySecret ID from the query.
|
||||||
|
// Returns a *NotFoundError when no SecuritySecret ID was found.
|
||||||
|
func (_q *SecuritySecretQuery) FirstID(ctx context.Context) (id int64, err error) {
|
||||||
|
var ids []int64
|
||||||
|
if ids, err = _q.Limit(1).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryFirstID)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
err = &NotFoundError{securitysecret.Label}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return ids[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstIDX is like FirstID, but panics if an error occurs.
|
||||||
|
func (_q *SecuritySecretQuery) FirstIDX(ctx context.Context) int64 {
|
||||||
|
id, err := _q.FirstID(ctx)
|
||||||
|
if err != nil && !IsNotFound(err) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only returns a single SecuritySecret entity found by the query, ensuring it only returns one.
|
||||||
|
// Returns a *NotSingularError when more than one SecuritySecret entity is found.
|
||||||
|
// Returns a *NotFoundError when no SecuritySecret entities are found.
|
||||||
|
func (_q *SecuritySecretQuery) Only(ctx context.Context) (*SecuritySecret, error) {
|
||||||
|
nodes, err := _q.Limit(2).All(setContextOp(ctx, _q.ctx, ent.OpQueryOnly))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch len(nodes) {
|
||||||
|
case 1:
|
||||||
|
return nodes[0], nil
|
||||||
|
case 0:
|
||||||
|
return nil, &NotFoundError{securitysecret.Label}
|
||||||
|
default:
|
||||||
|
return nil, &NotSingularError{securitysecret.Label}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnlyX is like Only, but panics if an error occurs.
|
||||||
|
func (_q *SecuritySecretQuery) OnlyX(ctx context.Context) *SecuritySecret {
|
||||||
|
node, err := _q.Only(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnlyID is like Only, but returns the only SecuritySecret ID in the query.
|
||||||
|
// Returns a *NotSingularError when more than one SecuritySecret ID is found.
|
||||||
|
// Returns a *NotFoundError when no entities are found.
|
||||||
|
func (_q *SecuritySecretQuery) OnlyID(ctx context.Context) (id int64, err error) {
|
||||||
|
var ids []int64
|
||||||
|
if ids, err = _q.Limit(2).IDs(setContextOp(ctx, _q.ctx, ent.OpQueryOnlyID)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch len(ids) {
|
||||||
|
case 1:
|
||||||
|
id = ids[0]
|
||||||
|
case 0:
|
||||||
|
err = &NotFoundError{securitysecret.Label}
|
||||||
|
default:
|
||||||
|
err = &NotSingularError{securitysecret.Label}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnlyIDX is like OnlyID, but panics if an error occurs.
|
||||||
|
func (_q *SecuritySecretQuery) OnlyIDX(ctx context.Context) int64 {
|
||||||
|
id, err := _q.OnlyID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// All executes the query and returns a list of SecuritySecrets.
|
||||||
|
func (_q *SecuritySecretQuery) All(ctx context.Context) ([]*SecuritySecret, error) {
|
||||||
|
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryAll)
|
||||||
|
if err := _q.prepareQuery(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
qr := querierAll[[]*SecuritySecret, *SecuritySecretQuery]()
|
||||||
|
return withInterceptors[[]*SecuritySecret](ctx, _q, qr, _q.inters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllX is like All, but panics if an error occurs.
|
||||||
|
func (_q *SecuritySecretQuery) AllX(ctx context.Context) []*SecuritySecret {
|
||||||
|
nodes, err := _q.All(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDs executes the query and returns a list of SecuritySecret IDs.
|
||||||
|
func (_q *SecuritySecretQuery) IDs(ctx context.Context) (ids []int64, err error) {
|
||||||
|
if _q.ctx.Unique == nil && _q.path != nil {
|
||||||
|
_q.Unique(true)
|
||||||
|
}
|
||||||
|
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryIDs)
|
||||||
|
if err = _q.Select(securitysecret.FieldID).Scan(ctx, &ids); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDsX is like IDs, but panics if an error occurs.
|
||||||
|
func (_q *SecuritySecretQuery) IDsX(ctx context.Context) []int64 {
|
||||||
|
ids, err := _q.IDs(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the count of the given query.
|
||||||
|
func (_q *SecuritySecretQuery) Count(ctx context.Context) (int, error) {
|
||||||
|
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryCount)
|
||||||
|
if err := _q.prepareQuery(ctx); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return withInterceptors[int](ctx, _q, querierCount[*SecuritySecretQuery](), _q.inters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountX is like Count, but panics if an error occurs.
|
||||||
|
func (_q *SecuritySecretQuery) CountX(ctx context.Context) int {
|
||||||
|
count, err := _q.Count(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exist returns true if the query has elements in the graph.
|
||||||
|
func (_q *SecuritySecretQuery) Exist(ctx context.Context) (bool, error) {
|
||||||
|
ctx = setContextOp(ctx, _q.ctx, ent.OpQueryExist)
|
||||||
|
switch _, err := _q.FirstID(ctx); {
|
||||||
|
case IsNotFound(err):
|
||||||
|
return false, nil
|
||||||
|
case err != nil:
|
||||||
|
return false, fmt.Errorf("ent: check existence: %w", err)
|
||||||
|
default:
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExistX is like Exist, but panics if an error occurs.
|
||||||
|
func (_q *SecuritySecretQuery) ExistX(ctx context.Context) bool {
|
||||||
|
exist, err := _q.Exist(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return exist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a duplicate of the SecuritySecretQuery builder, including all associated steps. It can be
|
||||||
|
// used to prepare common query builders and use them differently after the clone is made.
|
||||||
|
func (_q *SecuritySecretQuery) Clone() *SecuritySecretQuery {
|
||||||
|
if _q == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &SecuritySecretQuery{
|
||||||
|
config: _q.config,
|
||||||
|
ctx: _q.ctx.Clone(),
|
||||||
|
order: append([]securitysecret.OrderOption{}, _q.order...),
|
||||||
|
inters: append([]Interceptor{}, _q.inters...),
|
||||||
|
predicates: append([]predicate.SecuritySecret{}, _q.predicates...),
|
||||||
|
// clone intermediate query.
|
||||||
|
sql: _q.sql.Clone(),
|
||||||
|
path: _q.path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupBy is used to group vertices by one or more fields/columns.
|
||||||
|
// It is often used with aggregate functions, like: count, max, mean, min, sum.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// var v []struct {
|
||||||
|
// CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
|
// Count int `json:"count,omitempty"`
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// client.SecuritySecret.Query().
|
||||||
|
// GroupBy(securitysecret.FieldCreatedAt).
|
||||||
|
// Aggregate(ent.Count()).
|
||||||
|
// Scan(ctx, &v)
|
||||||
|
func (_q *SecuritySecretQuery) GroupBy(field string, fields ...string) *SecuritySecretGroupBy {
|
||||||
|
_q.ctx.Fields = append([]string{field}, fields...)
|
||||||
|
grbuild := &SecuritySecretGroupBy{build: _q}
|
||||||
|
grbuild.flds = &_q.ctx.Fields
|
||||||
|
grbuild.label = securitysecret.Label
|
||||||
|
grbuild.scan = grbuild.Scan
|
||||||
|
return grbuild
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select allows the selection one or more fields/columns for the given query,
|
||||||
|
// instead of selecting all fields in the entity.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// var v []struct {
|
||||||
|
// CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// client.SecuritySecret.Query().
|
||||||
|
// Select(securitysecret.FieldCreatedAt).
|
||||||
|
// Scan(ctx, &v)
|
||||||
|
func (_q *SecuritySecretQuery) Select(fields ...string) *SecuritySecretSelect {
|
||||||
|
_q.ctx.Fields = append(_q.ctx.Fields, fields...)
|
||||||
|
sbuild := &SecuritySecretSelect{SecuritySecretQuery: _q}
|
||||||
|
sbuild.label = securitysecret.Label
|
||||||
|
sbuild.flds, sbuild.scan = &_q.ctx.Fields, sbuild.Scan
|
||||||
|
return sbuild
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate returns a SecuritySecretSelect configured with the given aggregations.
|
||||||
|
func (_q *SecuritySecretQuery) Aggregate(fns ...AggregateFunc) *SecuritySecretSelect {
|
||||||
|
return _q.Select().Aggregate(fns...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_q *SecuritySecretQuery) prepareQuery(ctx context.Context) error {
|
||||||
|
for _, inter := range _q.inters {
|
||||||
|
if inter == nil {
|
||||||
|
return fmt.Errorf("ent: uninitialized interceptor (forgotten import ent/runtime?)")
|
||||||
|
}
|
||||||
|
if trv, ok := inter.(Traverser); ok {
|
||||||
|
if err := trv.Traverse(ctx, _q); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, f := range _q.ctx.Fields {
|
||||||
|
if !securitysecret.ValidColumn(f) {
|
||||||
|
return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _q.path != nil {
|
||||||
|
prev, err := _q.path(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_q.sql = prev
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_q *SecuritySecretQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*SecuritySecret, error) {
|
||||||
|
var (
|
||||||
|
nodes = []*SecuritySecret{}
|
||||||
|
_spec = _q.querySpec()
|
||||||
|
)
|
||||||
|
_spec.ScanValues = func(columns []string) ([]any, error) {
|
||||||
|
return (*SecuritySecret).scanValues(nil, columns)
|
||||||
|
}
|
||||||
|
_spec.Assign = func(columns []string, values []any) error {
|
||||||
|
node := &SecuritySecret{config: _q.config}
|
||||||
|
nodes = append(nodes, node)
|
||||||
|
return node.assignValues(columns, values)
|
||||||
|
}
|
||||||
|
if len(_q.modifiers) > 0 {
|
||||||
|
_spec.Modifiers = _q.modifiers
|
||||||
|
}
|
||||||
|
for i := range hooks {
|
||||||
|
hooks[i](ctx, _spec)
|
||||||
|
}
|
||||||
|
if err := sqlgraph.QueryNodes(ctx, _q.driver, _spec); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
return nodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_q *SecuritySecretQuery) sqlCount(ctx context.Context) (int, error) {
|
||||||
|
_spec := _q.querySpec()
|
||||||
|
if len(_q.modifiers) > 0 {
|
||||||
|
_spec.Modifiers = _q.modifiers
|
||||||
|
}
|
||||||
|
_spec.Node.Columns = _q.ctx.Fields
|
||||||
|
if len(_q.ctx.Fields) > 0 {
|
||||||
|
_spec.Unique = _q.ctx.Unique != nil && *_q.ctx.Unique
|
||||||
|
}
|
||||||
|
return sqlgraph.CountNodes(ctx, _q.driver, _spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_q *SecuritySecretQuery) querySpec() *sqlgraph.QuerySpec {
|
||||||
|
_spec := sqlgraph.NewQuerySpec(securitysecret.Table, securitysecret.Columns, sqlgraph.NewFieldSpec(securitysecret.FieldID, field.TypeInt64))
|
||||||
|
_spec.From = _q.sql
|
||||||
|
if unique := _q.ctx.Unique; unique != nil {
|
||||||
|
_spec.Unique = *unique
|
||||||
|
} else if _q.path != nil {
|
||||||
|
_spec.Unique = true
|
||||||
|
}
|
||||||
|
if fields := _q.ctx.Fields; len(fields) > 0 {
|
||||||
|
_spec.Node.Columns = make([]string, 0, len(fields))
|
||||||
|
_spec.Node.Columns = append(_spec.Node.Columns, securitysecret.FieldID)
|
||||||
|
for i := range fields {
|
||||||
|
if fields[i] != securitysecret.FieldID {
|
||||||
|
_spec.Node.Columns = append(_spec.Node.Columns, fields[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ps := _q.predicates; len(ps) > 0 {
|
||||||
|
_spec.Predicate = func(selector *sql.Selector) {
|
||||||
|
for i := range ps {
|
||||||
|
ps[i](selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if limit := _q.ctx.Limit; limit != nil {
|
||||||
|
_spec.Limit = *limit
|
||||||
|
}
|
||||||
|
if offset := _q.ctx.Offset; offset != nil {
|
||||||
|
_spec.Offset = *offset
|
||||||
|
}
|
||||||
|
if ps := _q.order; len(ps) > 0 {
|
||||||
|
_spec.Order = func(selector *sql.Selector) {
|
||||||
|
for i := range ps {
|
||||||
|
ps[i](selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_q *SecuritySecretQuery) sqlQuery(ctx context.Context) *sql.Selector {
|
||||||
|
builder := sql.Dialect(_q.driver.Dialect())
|
||||||
|
t1 := builder.Table(securitysecret.Table)
|
||||||
|
columns := _q.ctx.Fields
|
||||||
|
if len(columns) == 0 {
|
||||||
|
columns = securitysecret.Columns
|
||||||
|
}
|
||||||
|
selector := builder.Select(t1.Columns(columns...)...).From(t1)
|
||||||
|
if _q.sql != nil {
|
||||||
|
selector = _q.sql
|
||||||
|
selector.Select(selector.Columns(columns...)...)
|
||||||
|
}
|
||||||
|
if _q.ctx.Unique != nil && *_q.ctx.Unique {
|
||||||
|
selector.Distinct()
|
||||||
|
}
|
||||||
|
for _, m := range _q.modifiers {
|
||||||
|
m(selector)
|
||||||
|
}
|
||||||
|
for _, p := range _q.predicates {
|
||||||
|
p(selector)
|
||||||
|
}
|
||||||
|
for _, p := range _q.order {
|
||||||
|
p(selector)
|
||||||
|
}
|
||||||
|
if offset := _q.ctx.Offset; offset != nil {
|
||||||
|
// limit is mandatory for offset clause. We start
|
||||||
|
// with default value, and override it below if needed.
|
||||||
|
selector.Offset(*offset).Limit(math.MaxInt32)
|
||||||
|
}
|
||||||
|
if limit := _q.ctx.Limit; limit != nil {
|
||||||
|
selector.Limit(*limit)
|
||||||
|
}
|
||||||
|
return selector
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForUpdate locks the selected rows against concurrent updates, and prevent them from being
|
||||||
|
// updated, deleted or "selected ... for update" by other sessions, until the transaction is
|
||||||
|
// either committed or rolled-back.
|
||||||
|
func (_q *SecuritySecretQuery) ForUpdate(opts ...sql.LockOption) *SecuritySecretQuery {
|
||||||
|
if _q.driver.Dialect() == dialect.Postgres {
|
||||||
|
_q.Unique(false)
|
||||||
|
}
|
||||||
|
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
|
||||||
|
s.ForUpdate(opts...)
|
||||||
|
})
|
||||||
|
return _q
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForShare behaves similarly to ForUpdate, except that it acquires a shared mode lock
|
||||||
|
// on any rows that are read. Other sessions can read the rows, but cannot modify them
|
||||||
|
// until your transaction commits.
|
||||||
|
func (_q *SecuritySecretQuery) ForShare(opts ...sql.LockOption) *SecuritySecretQuery {
|
||||||
|
if _q.driver.Dialect() == dialect.Postgres {
|
||||||
|
_q.Unique(false)
|
||||||
|
}
|
||||||
|
_q.modifiers = append(_q.modifiers, func(s *sql.Selector) {
|
||||||
|
s.ForShare(opts...)
|
||||||
|
})
|
||||||
|
return _q
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecuritySecretGroupBy is the group-by builder for SecuritySecret entities.
|
||||||
|
type SecuritySecretGroupBy struct {
|
||||||
|
selector
|
||||||
|
build *SecuritySecretQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate adds the given aggregation functions to the group-by query.
|
||||||
|
func (_g *SecuritySecretGroupBy) Aggregate(fns ...AggregateFunc) *SecuritySecretGroupBy {
|
||||||
|
_g.fns = append(_g.fns, fns...)
|
||||||
|
return _g
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan applies the selector query and scans the result into the given value.
|
||||||
|
func (_g *SecuritySecretGroupBy) Scan(ctx context.Context, v any) error {
|
||||||
|
ctx = setContextOp(ctx, _g.build.ctx, ent.OpQueryGroupBy)
|
||||||
|
if err := _g.build.prepareQuery(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return scanWithInterceptors[*SecuritySecretQuery, *SecuritySecretGroupBy](ctx, _g.build, _g, _g.build.inters, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_g *SecuritySecretGroupBy) sqlScan(ctx context.Context, root *SecuritySecretQuery, v any) error {
|
||||||
|
selector := root.sqlQuery(ctx).Select()
|
||||||
|
aggregation := make([]string, 0, len(_g.fns))
|
||||||
|
for _, fn := range _g.fns {
|
||||||
|
aggregation = append(aggregation, fn(selector))
|
||||||
|
}
|
||||||
|
if len(selector.SelectedColumns()) == 0 {
|
||||||
|
columns := make([]string, 0, len(*_g.flds)+len(_g.fns))
|
||||||
|
for _, f := range *_g.flds {
|
||||||
|
columns = append(columns, selector.C(f))
|
||||||
|
}
|
||||||
|
columns = append(columns, aggregation...)
|
||||||
|
selector.Select(columns...)
|
||||||
|
}
|
||||||
|
selector.GroupBy(selector.Columns(*_g.flds...)...)
|
||||||
|
if err := selector.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rows := &sql.Rows{}
|
||||||
|
query, args := selector.Query()
|
||||||
|
if err := _g.build.driver.Query(ctx, query, args, rows); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return sql.ScanSlice(rows, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecuritySecretSelect is the builder for selecting fields of SecuritySecret entities.
|
||||||
|
type SecuritySecretSelect struct {
|
||||||
|
*SecuritySecretQuery
|
||||||
|
selector
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate adds the given aggregation functions to the selector query.
|
||||||
|
func (_s *SecuritySecretSelect) Aggregate(fns ...AggregateFunc) *SecuritySecretSelect {
|
||||||
|
_s.fns = append(_s.fns, fns...)
|
||||||
|
return _s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan applies the selector query and scans the result into the given value.
|
||||||
|
func (_s *SecuritySecretSelect) Scan(ctx context.Context, v any) error {
|
||||||
|
ctx = setContextOp(ctx, _s.ctx, ent.OpQuerySelect)
|
||||||
|
if err := _s.prepareQuery(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return scanWithInterceptors[*SecuritySecretQuery, *SecuritySecretSelect](ctx, _s.SecuritySecretQuery, _s, _s.inters, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_s *SecuritySecretSelect) sqlScan(ctx context.Context, root *SecuritySecretQuery, v any) error {
|
||||||
|
selector := root.sqlQuery(ctx)
|
||||||
|
aggregation := make([]string, 0, len(_s.fns))
|
||||||
|
for _, fn := range _s.fns {
|
||||||
|
aggregation = append(aggregation, fn(selector))
|
||||||
|
}
|
||||||
|
switch n := len(*_s.selector.flds); {
|
||||||
|
case n == 0 && len(aggregation) > 0:
|
||||||
|
selector.Select(aggregation...)
|
||||||
|
case n != 0 && len(aggregation) > 0:
|
||||||
|
selector.AppendSelect(aggregation...)
|
||||||
|
}
|
||||||
|
rows := &sql.Rows{}
|
||||||
|
query, args := selector.Query()
|
||||||
|
if err := _s.driver.Query(ctx, query, args, rows); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return sql.ScanSlice(rows, v)
|
||||||
|
}
|
||||||
316
backend/ent/securitysecret_update.go
Normal file
316
backend/ent/securitysecret_update.go
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
// Code generated by ent, DO NOT EDIT.
|
||||||
|
|
||||||
|
package ent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"entgo.io/ent/dialect/sql"
|
||||||
|
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||||
|
"entgo.io/ent/schema/field"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/predicate"
|
||||||
|
"github.com/Wei-Shaw/sub2api/ent/securitysecret"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SecuritySecretUpdate is the builder for updating SecuritySecret entities.
|
||||||
|
type SecuritySecretUpdate struct {
|
||||||
|
config
|
||||||
|
hooks []Hook
|
||||||
|
mutation *SecuritySecretMutation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where appends a list predicates to the SecuritySecretUpdate builder.
|
||||||
|
func (_u *SecuritySecretUpdate) Where(ps ...predicate.SecuritySecret) *SecuritySecretUpdate {
|
||||||
|
_u.mutation.Where(ps...)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUpdatedAt sets the "updated_at" field.
|
||||||
|
func (_u *SecuritySecretUpdate) SetUpdatedAt(v time.Time) *SecuritySecretUpdate {
|
||||||
|
_u.mutation.SetUpdatedAt(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKey sets the "key" field.
|
||||||
|
func (_u *SecuritySecretUpdate) SetKey(v string) *SecuritySecretUpdate {
|
||||||
|
_u.mutation.SetKey(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableKey sets the "key" field if the given value is not nil.
|
||||||
|
func (_u *SecuritySecretUpdate) SetNillableKey(v *string) *SecuritySecretUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetKey(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValue sets the "value" field.
|
||||||
|
func (_u *SecuritySecretUpdate) SetValue(v string) *SecuritySecretUpdate {
|
||||||
|
_u.mutation.SetValue(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableValue sets the "value" field if the given value is not nil.
|
||||||
|
func (_u *SecuritySecretUpdate) SetNillableValue(v *string) *SecuritySecretUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetValue(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutation returns the SecuritySecretMutation object of the builder.
|
||||||
|
func (_u *SecuritySecretUpdate) Mutation() *SecuritySecretMutation {
|
||||||
|
return _u.mutation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save executes the query and returns the number of nodes affected by the update operation.
|
||||||
|
func (_u *SecuritySecretUpdate) Save(ctx context.Context) (int, error) {
|
||||||
|
_u.defaults()
|
||||||
|
return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveX is like Save, but panics if an error occurs.
|
||||||
|
func (_u *SecuritySecretUpdate) SaveX(ctx context.Context) int {
|
||||||
|
affected, err := _u.Save(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return affected
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes the query.
|
||||||
|
func (_u *SecuritySecretUpdate) Exec(ctx context.Context) error {
|
||||||
|
_, err := _u.Save(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
|
func (_u *SecuritySecretUpdate) ExecX(ctx context.Context) {
|
||||||
|
if err := _u.Exec(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaults sets the default values of the builder before save.
|
||||||
|
func (_u *SecuritySecretUpdate) defaults() {
|
||||||
|
if _, ok := _u.mutation.UpdatedAt(); !ok {
|
||||||
|
v := securitysecret.UpdateDefaultUpdatedAt()
|
||||||
|
_u.mutation.SetUpdatedAt(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check runs all checks and user-defined validators on the builder.
|
||||||
|
func (_u *SecuritySecretUpdate) check() error {
|
||||||
|
if v, ok := _u.mutation.Key(); ok {
|
||||||
|
if err := securitysecret.KeyValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "SecuritySecret.key": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := _u.mutation.Value(); ok {
|
||||||
|
if err := securitysecret.ValueValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "value", err: fmt.Errorf(`ent: validator failed for field "SecuritySecret.value": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_u *SecuritySecretUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||||
|
if err := _u.check(); err != nil {
|
||||||
|
return _node, err
|
||||||
|
}
|
||||||
|
_spec := sqlgraph.NewUpdateSpec(securitysecret.Table, securitysecret.Columns, sqlgraph.NewFieldSpec(securitysecret.FieldID, field.TypeInt64))
|
||||||
|
if ps := _u.mutation.predicates; len(ps) > 0 {
|
||||||
|
_spec.Predicate = func(selector *sql.Selector) {
|
||||||
|
for i := range ps {
|
||||||
|
ps[i](selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.UpdatedAt(); ok {
|
||||||
|
_spec.SetField(securitysecret.FieldUpdatedAt, field.TypeTime, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.Key(); ok {
|
||||||
|
_spec.SetField(securitysecret.FieldKey, field.TypeString, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.Value(); ok {
|
||||||
|
_spec.SetField(securitysecret.FieldValue, field.TypeString, value)
|
||||||
|
}
|
||||||
|
if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
|
||||||
|
if _, ok := err.(*sqlgraph.NotFoundError); ok {
|
||||||
|
err = &NotFoundError{securitysecret.Label}
|
||||||
|
} else if sqlgraph.IsConstraintError(err) {
|
||||||
|
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||||
|
}
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
_u.mutation.done = true
|
||||||
|
return _node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecuritySecretUpdateOne is the builder for updating a single SecuritySecret entity.
|
||||||
|
type SecuritySecretUpdateOne struct {
|
||||||
|
config
|
||||||
|
fields []string
|
||||||
|
hooks []Hook
|
||||||
|
mutation *SecuritySecretMutation
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUpdatedAt sets the "updated_at" field.
|
||||||
|
func (_u *SecuritySecretUpdateOne) SetUpdatedAt(v time.Time) *SecuritySecretUpdateOne {
|
||||||
|
_u.mutation.SetUpdatedAt(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKey sets the "key" field.
|
||||||
|
func (_u *SecuritySecretUpdateOne) SetKey(v string) *SecuritySecretUpdateOne {
|
||||||
|
_u.mutation.SetKey(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableKey sets the "key" field if the given value is not nil.
|
||||||
|
func (_u *SecuritySecretUpdateOne) SetNillableKey(v *string) *SecuritySecretUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetKey(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValue sets the "value" field.
|
||||||
|
func (_u *SecuritySecretUpdateOne) SetValue(v string) *SecuritySecretUpdateOne {
|
||||||
|
_u.mutation.SetValue(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableValue sets the "value" field if the given value is not nil.
|
||||||
|
func (_u *SecuritySecretUpdateOne) SetNillableValue(v *string) *SecuritySecretUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetValue(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutation returns the SecuritySecretMutation object of the builder.
|
||||||
|
func (_u *SecuritySecretUpdateOne) Mutation() *SecuritySecretMutation {
|
||||||
|
return _u.mutation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where appends a list predicates to the SecuritySecretUpdate builder.
|
||||||
|
func (_u *SecuritySecretUpdateOne) Where(ps ...predicate.SecuritySecret) *SecuritySecretUpdateOne {
|
||||||
|
_u.mutation.Where(ps...)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select allows selecting one or more fields (columns) of the returned entity.
|
||||||
|
// The default is selecting all fields defined in the entity schema.
|
||||||
|
func (_u *SecuritySecretUpdateOne) Select(field string, fields ...string) *SecuritySecretUpdateOne {
|
||||||
|
_u.fields = append([]string{field}, fields...)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save executes the query and returns the updated SecuritySecret entity.
|
||||||
|
func (_u *SecuritySecretUpdateOne) Save(ctx context.Context) (*SecuritySecret, error) {
|
||||||
|
_u.defaults()
|
||||||
|
return withHooks(ctx, _u.sqlSave, _u.mutation, _u.hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveX is like Save, but panics if an error occurs.
|
||||||
|
func (_u *SecuritySecretUpdateOne) SaveX(ctx context.Context) *SecuritySecret {
|
||||||
|
node, err := _u.Save(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes the query on the entity.
|
||||||
|
func (_u *SecuritySecretUpdateOne) Exec(ctx context.Context) error {
|
||||||
|
_, err := _u.Save(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecX is like Exec, but panics if an error occurs.
|
||||||
|
func (_u *SecuritySecretUpdateOne) ExecX(ctx context.Context) {
|
||||||
|
if err := _u.Exec(ctx); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaults sets the default values of the builder before save.
|
||||||
|
func (_u *SecuritySecretUpdateOne) defaults() {
|
||||||
|
if _, ok := _u.mutation.UpdatedAt(); !ok {
|
||||||
|
v := securitysecret.UpdateDefaultUpdatedAt()
|
||||||
|
_u.mutation.SetUpdatedAt(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check runs all checks and user-defined validators on the builder.
|
||||||
|
func (_u *SecuritySecretUpdateOne) check() error {
|
||||||
|
if v, ok := _u.mutation.Key(); ok {
|
||||||
|
if err := securitysecret.KeyValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "SecuritySecret.key": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := _u.mutation.Value(); ok {
|
||||||
|
if err := securitysecret.ValueValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "value", err: fmt.Errorf(`ent: validator failed for field "SecuritySecret.value": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_u *SecuritySecretUpdateOne) sqlSave(ctx context.Context) (_node *SecuritySecret, err error) {
|
||||||
|
if err := _u.check(); err != nil {
|
||||||
|
return _node, err
|
||||||
|
}
|
||||||
|
_spec := sqlgraph.NewUpdateSpec(securitysecret.Table, securitysecret.Columns, sqlgraph.NewFieldSpec(securitysecret.FieldID, field.TypeInt64))
|
||||||
|
id, ok := _u.mutation.ID()
|
||||||
|
if !ok {
|
||||||
|
return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "SecuritySecret.id" for update`)}
|
||||||
|
}
|
||||||
|
_spec.Node.ID.Value = id
|
||||||
|
if fields := _u.fields; len(fields) > 0 {
|
||||||
|
_spec.Node.Columns = make([]string, 0, len(fields))
|
||||||
|
_spec.Node.Columns = append(_spec.Node.Columns, securitysecret.FieldID)
|
||||||
|
for _, f := range fields {
|
||||||
|
if !securitysecret.ValidColumn(f) {
|
||||||
|
return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)}
|
||||||
|
}
|
||||||
|
if f != securitysecret.FieldID {
|
||||||
|
_spec.Node.Columns = append(_spec.Node.Columns, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ps := _u.mutation.predicates; len(ps) > 0 {
|
||||||
|
_spec.Predicate = func(selector *sql.Selector) {
|
||||||
|
for i := range ps {
|
||||||
|
ps[i](selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.UpdatedAt(); ok {
|
||||||
|
_spec.SetField(securitysecret.FieldUpdatedAt, field.TypeTime, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.Key(); ok {
|
||||||
|
_spec.SetField(securitysecret.FieldKey, field.TypeString, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.Value(); ok {
|
||||||
|
_spec.SetField(securitysecret.FieldValue, field.TypeString, value)
|
||||||
|
}
|
||||||
|
_node = &SecuritySecret{config: _u.config}
|
||||||
|
_spec.Assign = _node.assignValues
|
||||||
|
_spec.ScanValues = _node.scanValues
|
||||||
|
if err = sqlgraph.UpdateNode(ctx, _u.driver, _spec); err != nil {
|
||||||
|
if _, ok := err.(*sqlgraph.NotFoundError); ok {
|
||||||
|
err = &NotFoundError{securitysecret.Label}
|
||||||
|
} else if sqlgraph.IsConstraintError(err) {
|
||||||
|
err = &ConstraintError{msg: err.Error(), wrap: err}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_u.mutation.done = true
|
||||||
|
return _node, nil
|
||||||
|
}
|
||||||
@@ -36,6 +36,8 @@ type Tx struct {
|
|||||||
Proxy *ProxyClient
|
Proxy *ProxyClient
|
||||||
// RedeemCode is the client for interacting with the RedeemCode builders.
|
// RedeemCode is the client for interacting with the RedeemCode builders.
|
||||||
RedeemCode *RedeemCodeClient
|
RedeemCode *RedeemCodeClient
|
||||||
|
// SecuritySecret is the client for interacting with the SecuritySecret builders.
|
||||||
|
SecuritySecret *SecuritySecretClient
|
||||||
// Setting is the client for interacting with the Setting builders.
|
// Setting is the client for interacting with the Setting builders.
|
||||||
Setting *SettingClient
|
Setting *SettingClient
|
||||||
// UsageCleanupTask is the client for interacting with the UsageCleanupTask builders.
|
// UsageCleanupTask is the client for interacting with the UsageCleanupTask builders.
|
||||||
@@ -194,6 +196,7 @@ func (tx *Tx) init() {
|
|||||||
tx.PromoCodeUsage = NewPromoCodeUsageClient(tx.config)
|
tx.PromoCodeUsage = NewPromoCodeUsageClient(tx.config)
|
||||||
tx.Proxy = NewProxyClient(tx.config)
|
tx.Proxy = NewProxyClient(tx.config)
|
||||||
tx.RedeemCode = NewRedeemCodeClient(tx.config)
|
tx.RedeemCode = NewRedeemCodeClient(tx.config)
|
||||||
|
tx.SecuritySecret = NewSecuritySecretClient(tx.config)
|
||||||
tx.Setting = NewSettingClient(tx.config)
|
tx.Setting = NewSettingClient(tx.config)
|
||||||
tx.UsageCleanupTask = NewUsageCleanupTaskClient(tx.config)
|
tx.UsageCleanupTask = NewUsageCleanupTaskClient(tx.config)
|
||||||
tx.UsageLog = NewUsageLogClient(tx.config)
|
tx.UsageLog = NewUsageLogClient(tx.config)
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ type UsageLog struct {
|
|||||||
ImageCount int `json:"image_count,omitempty"`
|
ImageCount int `json:"image_count,omitempty"`
|
||||||
// ImageSize holds the value of the "image_size" field.
|
// ImageSize holds the value of the "image_size" field.
|
||||||
ImageSize *string `json:"image_size,omitempty"`
|
ImageSize *string `json:"image_size,omitempty"`
|
||||||
|
// MediaType holds the value of the "media_type" field.
|
||||||
|
MediaType *string `json:"media_type,omitempty"`
|
||||||
|
// CacheTTLOverridden holds the value of the "cache_ttl_overridden" field.
|
||||||
|
CacheTTLOverridden bool `json:"cache_ttl_overridden,omitempty"`
|
||||||
// CreatedAt holds the value of the "created_at" field.
|
// CreatedAt holds the value of the "created_at" field.
|
||||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||||
// Edges holds the relations/edges for other nodes in the graph.
|
// Edges holds the relations/edges for other nodes in the graph.
|
||||||
@@ -165,13 +169,13 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) {
|
|||||||
values := make([]any, len(columns))
|
values := make([]any, len(columns))
|
||||||
for i := range columns {
|
for i := range columns {
|
||||||
switch columns[i] {
|
switch columns[i] {
|
||||||
case usagelog.FieldStream:
|
case usagelog.FieldStream, usagelog.FieldCacheTTLOverridden:
|
||||||
values[i] = new(sql.NullBool)
|
values[i] = new(sql.NullBool)
|
||||||
case usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCacheCreationCost, usagelog.FieldCacheReadCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldRateMultiplier, usagelog.FieldAccountRateMultiplier:
|
case usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCacheCreationCost, usagelog.FieldCacheReadCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldRateMultiplier, usagelog.FieldAccountRateMultiplier:
|
||||||
values[i] = new(sql.NullFloat64)
|
values[i] = new(sql.NullFloat64)
|
||||||
case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount:
|
case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount:
|
||||||
values[i] = new(sql.NullInt64)
|
values[i] = new(sql.NullInt64)
|
||||||
case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldUserAgent, usagelog.FieldIPAddress, usagelog.FieldImageSize:
|
case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldUserAgent, usagelog.FieldIPAddress, usagelog.FieldImageSize, usagelog.FieldMediaType:
|
||||||
values[i] = new(sql.NullString)
|
values[i] = new(sql.NullString)
|
||||||
case usagelog.FieldCreatedAt:
|
case usagelog.FieldCreatedAt:
|
||||||
values[i] = new(sql.NullTime)
|
values[i] = new(sql.NullTime)
|
||||||
@@ -378,6 +382,19 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error {
|
|||||||
_m.ImageSize = new(string)
|
_m.ImageSize = new(string)
|
||||||
*_m.ImageSize = value.String
|
*_m.ImageSize = value.String
|
||||||
}
|
}
|
||||||
|
case usagelog.FieldMediaType:
|
||||||
|
if value, ok := values[i].(*sql.NullString); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field media_type", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.MediaType = new(string)
|
||||||
|
*_m.MediaType = value.String
|
||||||
|
}
|
||||||
|
case usagelog.FieldCacheTTLOverridden:
|
||||||
|
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field cache_ttl_overridden", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.CacheTTLOverridden = value.Bool
|
||||||
|
}
|
||||||
case usagelog.FieldCreatedAt:
|
case usagelog.FieldCreatedAt:
|
||||||
if value, ok := values[i].(*sql.NullTime); !ok {
|
if value, ok := values[i].(*sql.NullTime); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field created_at", values[i])
|
return fmt.Errorf("unexpected type %T for field created_at", values[i])
|
||||||
@@ -548,6 +565,14 @@ func (_m *UsageLog) String() string {
|
|||||||
builder.WriteString(*v)
|
builder.WriteString(*v)
|
||||||
}
|
}
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
if v := _m.MediaType; v != nil {
|
||||||
|
builder.WriteString("media_type=")
|
||||||
|
builder.WriteString(*v)
|
||||||
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
|
builder.WriteString("cache_ttl_overridden=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", _m.CacheTTLOverridden))
|
||||||
|
builder.WriteString(", ")
|
||||||
builder.WriteString("created_at=")
|
builder.WriteString("created_at=")
|
||||||
builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
|
builder.WriteString(_m.CreatedAt.Format(time.ANSIC))
|
||||||
builder.WriteByte(')')
|
builder.WriteByte(')')
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ const (
|
|||||||
FieldImageCount = "image_count"
|
FieldImageCount = "image_count"
|
||||||
// FieldImageSize holds the string denoting the image_size field in the database.
|
// FieldImageSize holds the string denoting the image_size field in the database.
|
||||||
FieldImageSize = "image_size"
|
FieldImageSize = "image_size"
|
||||||
|
// FieldMediaType holds the string denoting the media_type field in the database.
|
||||||
|
FieldMediaType = "media_type"
|
||||||
|
// FieldCacheTTLOverridden holds the string denoting the cache_ttl_overridden field in the database.
|
||||||
|
FieldCacheTTLOverridden = "cache_ttl_overridden"
|
||||||
// FieldCreatedAt holds the string denoting the created_at field in the database.
|
// FieldCreatedAt holds the string denoting the created_at field in the database.
|
||||||
FieldCreatedAt = "created_at"
|
FieldCreatedAt = "created_at"
|
||||||
// EdgeUser holds the string denoting the user edge name in mutations.
|
// EdgeUser holds the string denoting the user edge name in mutations.
|
||||||
@@ -155,6 +159,8 @@ var Columns = []string{
|
|||||||
FieldIPAddress,
|
FieldIPAddress,
|
||||||
FieldImageCount,
|
FieldImageCount,
|
||||||
FieldImageSize,
|
FieldImageSize,
|
||||||
|
FieldMediaType,
|
||||||
|
FieldCacheTTLOverridden,
|
||||||
FieldCreatedAt,
|
FieldCreatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,6 +217,10 @@ var (
|
|||||||
DefaultImageCount int
|
DefaultImageCount int
|
||||||
// ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
|
// ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
|
||||||
ImageSizeValidator func(string) error
|
ImageSizeValidator func(string) error
|
||||||
|
// MediaTypeValidator is a validator for the "media_type" field. It is called by the builders before save.
|
||||||
|
MediaTypeValidator func(string) error
|
||||||
|
// DefaultCacheTTLOverridden holds the default value on creation for the "cache_ttl_overridden" field.
|
||||||
|
DefaultCacheTTLOverridden bool
|
||||||
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
|
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
|
||||||
DefaultCreatedAt func() time.Time
|
DefaultCreatedAt func() time.Time
|
||||||
)
|
)
|
||||||
@@ -368,6 +378,16 @@ func ByImageSize(opts ...sql.OrderTermOption) OrderOption {
|
|||||||
return sql.OrderByField(FieldImageSize, opts...).ToFunc()
|
return sql.OrderByField(FieldImageSize, opts...).ToFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ByMediaType orders the results by the media_type field.
|
||||||
|
func ByMediaType(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldMediaType, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByCacheTTLOverridden orders the results by the cache_ttl_overridden field.
|
||||||
|
func ByCacheTTLOverridden(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldCacheTTLOverridden, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
// ByCreatedAt orders the results by the created_at field.
|
// ByCreatedAt orders the results by the created_at field.
|
||||||
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
|
func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption {
|
||||||
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
|
return sql.OrderByField(FieldCreatedAt, opts...).ToFunc()
|
||||||
|
|||||||
@@ -200,6 +200,16 @@ func ImageSize(v string) predicate.UsageLog {
|
|||||||
return predicate.UsageLog(sql.FieldEQ(FieldImageSize, v))
|
return predicate.UsageLog(sql.FieldEQ(FieldImageSize, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MediaType applies equality check predicate on the "media_type" field. It's identical to MediaTypeEQ.
|
||||||
|
func MediaType(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldEQ(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheTTLOverridden applies equality check predicate on the "cache_ttl_overridden" field. It's identical to CacheTTLOverriddenEQ.
|
||||||
|
func CacheTTLOverridden(v bool) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldEQ(FieldCacheTTLOverridden, v))
|
||||||
|
}
|
||||||
|
|
||||||
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
|
// CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ.
|
||||||
func CreatedAt(v time.Time) predicate.UsageLog {
|
func CreatedAt(v time.Time) predicate.UsageLog {
|
||||||
return predicate.UsageLog(sql.FieldEQ(FieldCreatedAt, v))
|
return predicate.UsageLog(sql.FieldEQ(FieldCreatedAt, v))
|
||||||
@@ -1440,6 +1450,91 @@ func ImageSizeContainsFold(v string) predicate.UsageLog {
|
|||||||
return predicate.UsageLog(sql.FieldContainsFold(FieldImageSize, v))
|
return predicate.UsageLog(sql.FieldContainsFold(FieldImageSize, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MediaTypeEQ applies the EQ predicate on the "media_type" field.
|
||||||
|
func MediaTypeEQ(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldEQ(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeNEQ applies the NEQ predicate on the "media_type" field.
|
||||||
|
func MediaTypeNEQ(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldNEQ(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeIn applies the In predicate on the "media_type" field.
|
||||||
|
func MediaTypeIn(vs ...string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldIn(FieldMediaType, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeNotIn applies the NotIn predicate on the "media_type" field.
|
||||||
|
func MediaTypeNotIn(vs ...string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldNotIn(FieldMediaType, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeGT applies the GT predicate on the "media_type" field.
|
||||||
|
func MediaTypeGT(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldGT(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeGTE applies the GTE predicate on the "media_type" field.
|
||||||
|
func MediaTypeGTE(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldGTE(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeLT applies the LT predicate on the "media_type" field.
|
||||||
|
func MediaTypeLT(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldLT(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeLTE applies the LTE predicate on the "media_type" field.
|
||||||
|
func MediaTypeLTE(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldLTE(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeContains applies the Contains predicate on the "media_type" field.
|
||||||
|
func MediaTypeContains(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldContains(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeHasPrefix applies the HasPrefix predicate on the "media_type" field.
|
||||||
|
func MediaTypeHasPrefix(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldHasPrefix(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeHasSuffix applies the HasSuffix predicate on the "media_type" field.
|
||||||
|
func MediaTypeHasSuffix(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldHasSuffix(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeIsNil applies the IsNil predicate on the "media_type" field.
|
||||||
|
func MediaTypeIsNil() predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldIsNull(FieldMediaType))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeNotNil applies the NotNil predicate on the "media_type" field.
|
||||||
|
func MediaTypeNotNil() predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldNotNull(FieldMediaType))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeEqualFold applies the EqualFold predicate on the "media_type" field.
|
||||||
|
func MediaTypeEqualFold(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldEqualFold(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaTypeContainsFold applies the ContainsFold predicate on the "media_type" field.
|
||||||
|
func MediaTypeContainsFold(v string) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldContainsFold(FieldMediaType, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheTTLOverriddenEQ applies the EQ predicate on the "cache_ttl_overridden" field.
|
||||||
|
func CacheTTLOverriddenEQ(v bool) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldEQ(FieldCacheTTLOverridden, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheTTLOverriddenNEQ applies the NEQ predicate on the "cache_ttl_overridden" field.
|
||||||
|
func CacheTTLOverriddenNEQ(v bool) predicate.UsageLog {
|
||||||
|
return predicate.UsageLog(sql.FieldNEQ(FieldCacheTTLOverridden, v))
|
||||||
|
}
|
||||||
|
|
||||||
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
|
||||||
func CreatedAtEQ(v time.Time) predicate.UsageLog {
|
func CreatedAtEQ(v time.Time) predicate.UsageLog {
|
||||||
return predicate.UsageLog(sql.FieldEQ(FieldCreatedAt, v))
|
return predicate.UsageLog(sql.FieldEQ(FieldCreatedAt, v))
|
||||||
|
|||||||
@@ -393,6 +393,34 @@ func (_c *UsageLogCreate) SetNillableImageSize(v *string) *UsageLogCreate {
|
|||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMediaType sets the "media_type" field.
|
||||||
|
func (_c *UsageLogCreate) SetMediaType(v string) *UsageLogCreate {
|
||||||
|
_c.mutation.SetMediaType(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableMediaType sets the "media_type" field if the given value is not nil.
|
||||||
|
func (_c *UsageLogCreate) SetNillableMediaType(v *string) *UsageLogCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetMediaType(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||||
|
func (_c *UsageLogCreate) SetCacheTTLOverridden(v bool) *UsageLogCreate {
|
||||||
|
_c.mutation.SetCacheTTLOverridden(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableCacheTTLOverridden sets the "cache_ttl_overridden" field if the given value is not nil.
|
||||||
|
func (_c *UsageLogCreate) SetNillableCacheTTLOverridden(v *bool) *UsageLogCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetCacheTTLOverridden(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SetCreatedAt sets the "created_at" field.
|
// SetCreatedAt sets the "created_at" field.
|
||||||
func (_c *UsageLogCreate) SetCreatedAt(v time.Time) *UsageLogCreate {
|
func (_c *UsageLogCreate) SetCreatedAt(v time.Time) *UsageLogCreate {
|
||||||
_c.mutation.SetCreatedAt(v)
|
_c.mutation.SetCreatedAt(v)
|
||||||
@@ -531,6 +559,10 @@ func (_c *UsageLogCreate) defaults() {
|
|||||||
v := usagelog.DefaultImageCount
|
v := usagelog.DefaultImageCount
|
||||||
_c.mutation.SetImageCount(v)
|
_c.mutation.SetImageCount(v)
|
||||||
}
|
}
|
||||||
|
if _, ok := _c.mutation.CacheTTLOverridden(); !ok {
|
||||||
|
v := usagelog.DefaultCacheTTLOverridden
|
||||||
|
_c.mutation.SetCacheTTLOverridden(v)
|
||||||
|
}
|
||||||
if _, ok := _c.mutation.CreatedAt(); !ok {
|
if _, ok := _c.mutation.CreatedAt(); !ok {
|
||||||
v := usagelog.DefaultCreatedAt()
|
v := usagelog.DefaultCreatedAt()
|
||||||
_c.mutation.SetCreatedAt(v)
|
_c.mutation.SetCreatedAt(v)
|
||||||
@@ -627,6 +659,14 @@ func (_c *UsageLogCreate) check() error {
|
|||||||
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v, ok := _c.mutation.MediaType(); ok {
|
||||||
|
if err := usagelog.MediaTypeValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "media_type", err: fmt.Errorf(`ent: validator failed for field "UsageLog.media_type": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := _c.mutation.CacheTTLOverridden(); !ok {
|
||||||
|
return &ValidationError{Name: "cache_ttl_overridden", err: errors.New(`ent: missing required field "UsageLog.cache_ttl_overridden"`)}
|
||||||
|
}
|
||||||
if _, ok := _c.mutation.CreatedAt(); !ok {
|
if _, ok := _c.mutation.CreatedAt(); !ok {
|
||||||
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "UsageLog.created_at"`)}
|
return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "UsageLog.created_at"`)}
|
||||||
}
|
}
|
||||||
@@ -762,6 +802,14 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) {
|
|||||||
_spec.SetField(usagelog.FieldImageSize, field.TypeString, value)
|
_spec.SetField(usagelog.FieldImageSize, field.TypeString, value)
|
||||||
_node.ImageSize = &value
|
_node.ImageSize = &value
|
||||||
}
|
}
|
||||||
|
if value, ok := _c.mutation.MediaType(); ok {
|
||||||
|
_spec.SetField(usagelog.FieldMediaType, field.TypeString, value)
|
||||||
|
_node.MediaType = &value
|
||||||
|
}
|
||||||
|
if value, ok := _c.mutation.CacheTTLOverridden(); ok {
|
||||||
|
_spec.SetField(usagelog.FieldCacheTTLOverridden, field.TypeBool, value)
|
||||||
|
_node.CacheTTLOverridden = value
|
||||||
|
}
|
||||||
if value, ok := _c.mutation.CreatedAt(); ok {
|
if value, ok := _c.mutation.CreatedAt(); ok {
|
||||||
_spec.SetField(usagelog.FieldCreatedAt, field.TypeTime, value)
|
_spec.SetField(usagelog.FieldCreatedAt, field.TypeTime, value)
|
||||||
_node.CreatedAt = value
|
_node.CreatedAt = value
|
||||||
@@ -1407,6 +1455,36 @@ func (u *UsageLogUpsert) ClearImageSize() *UsageLogUpsert {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMediaType sets the "media_type" field.
|
||||||
|
func (u *UsageLogUpsert) SetMediaType(v string) *UsageLogUpsert {
|
||||||
|
u.Set(usagelog.FieldMediaType, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaType sets the "media_type" field to the value that was provided on create.
|
||||||
|
func (u *UsageLogUpsert) UpdateMediaType() *UsageLogUpsert {
|
||||||
|
u.SetExcluded(usagelog.FieldMediaType)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearMediaType clears the value of the "media_type" field.
|
||||||
|
func (u *UsageLogUpsert) ClearMediaType() *UsageLogUpsert {
|
||||||
|
u.SetNull(usagelog.FieldMediaType)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||||
|
func (u *UsageLogUpsert) SetCacheTTLOverridden(v bool) *UsageLogUpsert {
|
||||||
|
u.Set(usagelog.FieldCacheTTLOverridden, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCacheTTLOverridden sets the "cache_ttl_overridden" field to the value that was provided on create.
|
||||||
|
func (u *UsageLogUpsert) UpdateCacheTTLOverridden() *UsageLogUpsert {
|
||||||
|
u.SetExcluded(usagelog.FieldCacheTTLOverridden)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateNewValues updates the mutable fields using the new values that were set on create.
|
// UpdateNewValues updates the mutable fields using the new values that were set on create.
|
||||||
// Using this option is equivalent to using:
|
// Using this option is equivalent to using:
|
||||||
//
|
//
|
||||||
@@ -2040,6 +2118,41 @@ func (u *UsageLogUpsertOne) ClearImageSize() *UsageLogUpsertOne {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMediaType sets the "media_type" field.
|
||||||
|
func (u *UsageLogUpsertOne) SetMediaType(v string) *UsageLogUpsertOne {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.SetMediaType(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaType sets the "media_type" field to the value that was provided on create.
|
||||||
|
func (u *UsageLogUpsertOne) UpdateMediaType() *UsageLogUpsertOne {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.UpdateMediaType()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearMediaType clears the value of the "media_type" field.
|
||||||
|
func (u *UsageLogUpsertOne) ClearMediaType() *UsageLogUpsertOne {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.ClearMediaType()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||||
|
func (u *UsageLogUpsertOne) SetCacheTTLOverridden(v bool) *UsageLogUpsertOne {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.SetCacheTTLOverridden(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCacheTTLOverridden sets the "cache_ttl_overridden" field to the value that was provided on create.
|
||||||
|
func (u *UsageLogUpsertOne) UpdateCacheTTLOverridden() *UsageLogUpsertOne {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.UpdateCacheTTLOverridden()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Exec executes the query.
|
// Exec executes the query.
|
||||||
func (u *UsageLogUpsertOne) Exec(ctx context.Context) error {
|
func (u *UsageLogUpsertOne) Exec(ctx context.Context) error {
|
||||||
if len(u.create.conflict) == 0 {
|
if len(u.create.conflict) == 0 {
|
||||||
@@ -2839,6 +2952,41 @@ func (u *UsageLogUpsertBulk) ClearImageSize() *UsageLogUpsertBulk {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMediaType sets the "media_type" field.
|
||||||
|
func (u *UsageLogUpsertBulk) SetMediaType(v string) *UsageLogUpsertBulk {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.SetMediaType(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMediaType sets the "media_type" field to the value that was provided on create.
|
||||||
|
func (u *UsageLogUpsertBulk) UpdateMediaType() *UsageLogUpsertBulk {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.UpdateMediaType()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearMediaType clears the value of the "media_type" field.
|
||||||
|
func (u *UsageLogUpsertBulk) ClearMediaType() *UsageLogUpsertBulk {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.ClearMediaType()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||||
|
func (u *UsageLogUpsertBulk) SetCacheTTLOverridden(v bool) *UsageLogUpsertBulk {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.SetCacheTTLOverridden(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCacheTTLOverridden sets the "cache_ttl_overridden" field to the value that was provided on create.
|
||||||
|
func (u *UsageLogUpsertBulk) UpdateCacheTTLOverridden() *UsageLogUpsertBulk {
|
||||||
|
return u.Update(func(s *UsageLogUpsert) {
|
||||||
|
s.UpdateCacheTTLOverridden()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Exec executes the query.
|
// Exec executes the query.
|
||||||
func (u *UsageLogUpsertBulk) Exec(ctx context.Context) error {
|
func (u *UsageLogUpsertBulk) Exec(ctx context.Context) error {
|
||||||
if u.create.err != nil {
|
if u.create.err != nil {
|
||||||
|
|||||||
@@ -612,6 +612,40 @@ func (_u *UsageLogUpdate) ClearImageSize() *UsageLogUpdate {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMediaType sets the "media_type" field.
|
||||||
|
func (_u *UsageLogUpdate) SetMediaType(v string) *UsageLogUpdate {
|
||||||
|
_u.mutation.SetMediaType(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableMediaType sets the "media_type" field if the given value is not nil.
|
||||||
|
func (_u *UsageLogUpdate) SetNillableMediaType(v *string) *UsageLogUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetMediaType(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearMediaType clears the value of the "media_type" field.
|
||||||
|
func (_u *UsageLogUpdate) ClearMediaType() *UsageLogUpdate {
|
||||||
|
_u.mutation.ClearMediaType()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||||
|
func (_u *UsageLogUpdate) SetCacheTTLOverridden(v bool) *UsageLogUpdate {
|
||||||
|
_u.mutation.SetCacheTTLOverridden(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableCacheTTLOverridden sets the "cache_ttl_overridden" field if the given value is not nil.
|
||||||
|
func (_u *UsageLogUpdate) SetNillableCacheTTLOverridden(v *bool) *UsageLogUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetCacheTTLOverridden(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetUser sets the "user" edge to the User entity.
|
// SetUser sets the "user" edge to the User entity.
|
||||||
func (_u *UsageLogUpdate) SetUser(v *User) *UsageLogUpdate {
|
func (_u *UsageLogUpdate) SetUser(v *User) *UsageLogUpdate {
|
||||||
return _u.SetUserID(v.ID)
|
return _u.SetUserID(v.ID)
|
||||||
@@ -726,6 +760,11 @@ func (_u *UsageLogUpdate) check() error {
|
|||||||
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v, ok := _u.mutation.MediaType(); ok {
|
||||||
|
if err := usagelog.MediaTypeValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "media_type", err: fmt.Errorf(`ent: validator failed for field "UsageLog.media_type": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
|
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
|
||||||
return errors.New(`ent: clearing a required unique edge "UsageLog.user"`)
|
return errors.New(`ent: clearing a required unique edge "UsageLog.user"`)
|
||||||
}
|
}
|
||||||
@@ -894,6 +933,15 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
|||||||
if _u.mutation.ImageSizeCleared() {
|
if _u.mutation.ImageSizeCleared() {
|
||||||
_spec.ClearField(usagelog.FieldImageSize, field.TypeString)
|
_spec.ClearField(usagelog.FieldImageSize, field.TypeString)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.MediaType(); ok {
|
||||||
|
_spec.SetField(usagelog.FieldMediaType, field.TypeString, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.MediaTypeCleared() {
|
||||||
|
_spec.ClearField(usagelog.FieldMediaType, field.TypeString)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.CacheTTLOverridden(); ok {
|
||||||
|
_spec.SetField(usagelog.FieldCacheTTLOverridden, field.TypeBool, value)
|
||||||
|
}
|
||||||
if _u.mutation.UserCleared() {
|
if _u.mutation.UserCleared() {
|
||||||
edge := &sqlgraph.EdgeSpec{
|
edge := &sqlgraph.EdgeSpec{
|
||||||
Rel: sqlgraph.M2O,
|
Rel: sqlgraph.M2O,
|
||||||
@@ -1639,6 +1687,40 @@ func (_u *UsageLogUpdateOne) ClearImageSize() *UsageLogUpdateOne {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMediaType sets the "media_type" field.
|
||||||
|
func (_u *UsageLogUpdateOne) SetMediaType(v string) *UsageLogUpdateOne {
|
||||||
|
_u.mutation.SetMediaType(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableMediaType sets the "media_type" field if the given value is not nil.
|
||||||
|
func (_u *UsageLogUpdateOne) SetNillableMediaType(v *string) *UsageLogUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetMediaType(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearMediaType clears the value of the "media_type" field.
|
||||||
|
func (_u *UsageLogUpdateOne) ClearMediaType() *UsageLogUpdateOne {
|
||||||
|
_u.mutation.ClearMediaType()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||||
|
func (_u *UsageLogUpdateOne) SetCacheTTLOverridden(v bool) *UsageLogUpdateOne {
|
||||||
|
_u.mutation.SetCacheTTLOverridden(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableCacheTTLOverridden sets the "cache_ttl_overridden" field if the given value is not nil.
|
||||||
|
func (_u *UsageLogUpdateOne) SetNillableCacheTTLOverridden(v *bool) *UsageLogUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetCacheTTLOverridden(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetUser sets the "user" edge to the User entity.
|
// SetUser sets the "user" edge to the User entity.
|
||||||
func (_u *UsageLogUpdateOne) SetUser(v *User) *UsageLogUpdateOne {
|
func (_u *UsageLogUpdateOne) SetUser(v *User) *UsageLogUpdateOne {
|
||||||
return _u.SetUserID(v.ID)
|
return _u.SetUserID(v.ID)
|
||||||
@@ -1766,6 +1848,11 @@ func (_u *UsageLogUpdateOne) check() error {
|
|||||||
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v, ok := _u.mutation.MediaType(); ok {
|
||||||
|
if err := usagelog.MediaTypeValidator(v); err != nil {
|
||||||
|
return &ValidationError{Name: "media_type", err: fmt.Errorf(`ent: validator failed for field "UsageLog.media_type": %w`, err)}
|
||||||
|
}
|
||||||
|
}
|
||||||
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
|
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
|
||||||
return errors.New(`ent: clearing a required unique edge "UsageLog.user"`)
|
return errors.New(`ent: clearing a required unique edge "UsageLog.user"`)
|
||||||
}
|
}
|
||||||
@@ -1951,6 +2038,15 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err
|
|||||||
if _u.mutation.ImageSizeCleared() {
|
if _u.mutation.ImageSizeCleared() {
|
||||||
_spec.ClearField(usagelog.FieldImageSize, field.TypeString)
|
_spec.ClearField(usagelog.FieldImageSize, field.TypeString)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.MediaType(); ok {
|
||||||
|
_spec.SetField(usagelog.FieldMediaType, field.TypeString, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.MediaTypeCleared() {
|
||||||
|
_spec.ClearField(usagelog.FieldMediaType, field.TypeString)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.CacheTTLOverridden(); ok {
|
||||||
|
_spec.SetField(usagelog.FieldCacheTTLOverridden, field.TypeBool, value)
|
||||||
|
}
|
||||||
if _u.mutation.UserCleared() {
|
if _u.mutation.UserCleared() {
|
||||||
edge := &sqlgraph.EdgeSpec{
|
edge := &sqlgraph.EdgeSpec{
|
||||||
Rel: sqlgraph.M2O,
|
Rel: sqlgraph.M2O,
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ go 1.25.7
|
|||||||
require (
|
require (
|
||||||
entgo.io/ent v0.14.5
|
entgo.io/ent v0.14.5
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
|
github.com/DouDOU-start/go-sora2api v1.1.0
|
||||||
|
github.com/alitto/pond/v2 v2.6.2
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0
|
||||||
github.com/dgraph-io/ristretto v0.2.0
|
github.com/dgraph-io/ristretto v0.2.0
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
@@ -13,9 +16,10 @@ require (
|
|||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/imroc/req/v3 v3.57.0
|
github.com/imroc/req/v3 v3.57.0
|
||||||
github.com/lib/pq v1.10.9
|
github.com/lib/pq v1.10.9
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/redis/go-redis/v9 v9.17.2
|
github.com/redis/go-redis/v9 v9.17.2
|
||||||
github.com/refraction-networking/utls v1.8.1
|
github.com/refraction-networking/utls v1.8.2
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shirou/gopsutil/v4 v4.25.6
|
github.com/shirou/gopsutil/v4 v4.25.6
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
@@ -25,10 +29,12 @@ require (
|
|||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/tidwall/sjson v1.2.5
|
github.com/tidwall/sjson v1.2.5
|
||||||
github.com/zeromicro/go-zero v1.9.4
|
github.com/zeromicro/go-zero v1.9.4
|
||||||
golang.org/x/crypto v0.47.0
|
go.uber.org/zap v1.24.0
|
||||||
|
golang.org/x/crypto v0.48.0
|
||||||
golang.org/x/net v0.49.0
|
golang.org/x/net v0.49.0
|
||||||
golang.org/x/sync v0.19.0
|
golang.org/x/sync v0.19.0
|
||||||
golang.org/x/term v0.39.0
|
golang.org/x/term v0.40.0
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
modernc.org/sqlite v1.44.3
|
modernc.org/sqlite v1.44.3
|
||||||
)
|
)
|
||||||
@@ -41,11 +47,17 @@ require (
|
|||||||
github.com/agext/levenshtein v1.2.3 // indirect
|
github.com/agext/levenshtein v1.2.3 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
|
github.com/bdandy/go-errors v1.2.2 // indirect
|
||||||
|
github.com/bdandy/go-socks4 v1.2.3 // indirect
|
||||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||||
|
github.com/bogdanfinn/fhttp v0.6.8 // indirect
|
||||||
|
github.com/bogdanfinn/quic-go-utls v1.0.9-utls // indirect
|
||||||
|
github.com/bogdanfinn/tls-client v1.14.0 // indirect
|
||||||
|
github.com/bogdanfinn/utls v1.7.7-barnius // indirect
|
||||||
|
github.com/bogdanfinn/websocket v1.5.5-barnius // indirect
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
@@ -103,7 +115,6 @@ require (
|
|||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
@@ -120,6 +131,7 @@ require (
|
|||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect
|
||||||
github.com/testcontainers/testcontainers-go v0.40.0 // indirect
|
github.com/testcontainers/testcontainers-go v0.40.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
@@ -141,9 +153,9 @@ require (
|
|||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/mod v0.31.0 // indirect
|
golang.org/x/mod v0.32.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
google.golang.org/grpc v1.75.1 // indirect
|
google.golang.org/grpc v1.75.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
|||||||
@@ -10,16 +10,36 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
|
github.com/DouDOU-start/go-sora2api v1.1.0 h1:PxWiukK77StiHxEngOFwT1rKUn9oTAJJTl07wQUXwiU=
|
||||||
|
github.com/DouDOU-start/go-sora2api v1.1.0/go.mod h1:dcwpethoKfAsMWskDD9iGgc/3yox2tkthPLSMVGnhkE=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||||
|
github.com/alitto/pond/v2 v2.6.2 h1:Sphe40g0ILeM1pA2c2K+Th0DGU+pt0A/Kprr+WB24Pw=
|
||||||
|
github.com/alitto/pond/v2 v2.6.2/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||||
|
github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q=
|
||||||
|
github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM=
|
||||||
|
github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic=
|
||||||
|
github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI=
|
||||||
|
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||||
|
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
||||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||||
|
github.com/bogdanfinn/fhttp v0.6.8 h1:LiQyHOY3i0QoxxNB7nq27/nGNNbtPj0fuBPozhR7Ws4=
|
||||||
|
github.com/bogdanfinn/fhttp v0.6.8/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M=
|
||||||
|
github.com/bogdanfinn/quic-go-utls v1.0.9-utls h1:tV6eDEiRbRCcepALSzxR94JUVD3N3ACIiRLgyc2Ep8s=
|
||||||
|
github.com/bogdanfinn/quic-go-utls v1.0.9-utls/go.mod h1:aHph9B9H9yPOt5xnhWKSOum27DJAqpiHzwX+gjvaXcg=
|
||||||
|
github.com/bogdanfinn/tls-client v1.14.0 h1:vyk7Cn4BIvLAGVuMfb0tP22OqogfO1lYamquQNEZU1A=
|
||||||
|
github.com/bogdanfinn/tls-client v1.14.0/go.mod h1:LsU6mXVn8MOFDwTkyRfI7V1BZM1p0wf2ZfZsICW/1fM=
|
||||||
|
github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU=
|
||||||
|
github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg=
|
||||||
|
github.com/bogdanfinn/websocket v1.5.5-barnius h1:bY+qnxpai1qe7Jmjx+Sds/cmOSpuuLoR8x61rWltjOI=
|
||||||
|
github.com/bogdanfinn/websocket v1.5.5-barnius/go.mod h1:gvvEw6pTKHb7yOiFvIfAFTStQWyrm25BMVCTj5wRSsI=
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
@@ -135,8 +155,6 @@ github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
|||||||
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||||
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
||||||
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
|
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
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/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 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@@ -172,8 +190,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
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-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
|
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
|
||||||
@@ -207,8 +223,6 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
|
||||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
@@ -234,12 +248,10 @@ github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI1
|
|||||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
|
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||||
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
@@ -262,8 +274,6 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
|||||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
|
||||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||||
@@ -285,6 +295,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc=
|
||||||
|
github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng=
|
||||||
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
||||||
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk=
|
github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk=
|
||||||
@@ -340,25 +352,32 @@ go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
|||||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
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=
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
|
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||||
|
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -366,16 +385,19 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
||||||
@@ -391,6 +413,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
|||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -19,6 +19,13 @@ const (
|
|||||||
RunModeSimple = "simple"
|
RunModeSimple = "simple"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 使用量记录队列溢出策略
|
||||||
|
const (
|
||||||
|
UsageRecordOverflowPolicyDrop = "drop"
|
||||||
|
UsageRecordOverflowPolicySample = "sample"
|
||||||
|
UsageRecordOverflowPolicySync = "sync"
|
||||||
|
)
|
||||||
|
|
||||||
// DefaultCSPPolicy is the default Content-Security-Policy with nonce support
|
// DefaultCSPPolicy is the default Content-Security-Policy with nonce support
|
||||||
// __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
|
// __CSP_NONCE__ will be replaced with actual nonce at request time by the SecurityHeaders middleware
|
||||||
const DefaultCSPPolicy = "default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
const DefaultCSPPolicy = "default-src 'self'; script-src 'self' __CSP_NONCE__ https://challenges.cloudflare.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:; frame-src https://challenges.cloudflare.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
|
||||||
@@ -38,31 +45,68 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `mapstructure:"server"`
|
Server ServerConfig `mapstructure:"server"`
|
||||||
CORS CORSConfig `mapstructure:"cors"`
|
Log LogConfig `mapstructure:"log"`
|
||||||
Security SecurityConfig `mapstructure:"security"`
|
CORS CORSConfig `mapstructure:"cors"`
|
||||||
Billing BillingConfig `mapstructure:"billing"`
|
Security SecurityConfig `mapstructure:"security"`
|
||||||
Turnstile TurnstileConfig `mapstructure:"turnstile"`
|
Billing BillingConfig `mapstructure:"billing"`
|
||||||
Database DatabaseConfig `mapstructure:"database"`
|
Turnstile TurnstileConfig `mapstructure:"turnstile"`
|
||||||
Redis RedisConfig `mapstructure:"redis"`
|
Database DatabaseConfig `mapstructure:"database"`
|
||||||
Ops OpsConfig `mapstructure:"ops"`
|
Redis RedisConfig `mapstructure:"redis"`
|
||||||
JWT JWTConfig `mapstructure:"jwt"`
|
Ops OpsConfig `mapstructure:"ops"`
|
||||||
Totp TotpConfig `mapstructure:"totp"`
|
JWT JWTConfig `mapstructure:"jwt"`
|
||||||
LinuxDo LinuxDoConnectConfig `mapstructure:"linuxdo_connect"`
|
Totp TotpConfig `mapstructure:"totp"`
|
||||||
Default DefaultConfig `mapstructure:"default"`
|
LinuxDo LinuxDoConnectConfig `mapstructure:"linuxdo_connect"`
|
||||||
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
|
Default DefaultConfig `mapstructure:"default"`
|
||||||
Pricing PricingConfig `mapstructure:"pricing"`
|
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
|
||||||
Gateway GatewayConfig `mapstructure:"gateway"`
|
Pricing PricingConfig `mapstructure:"pricing"`
|
||||||
APIKeyAuth APIKeyAuthCacheConfig `mapstructure:"api_key_auth_cache"`
|
Gateway GatewayConfig `mapstructure:"gateway"`
|
||||||
Dashboard DashboardCacheConfig `mapstructure:"dashboard_cache"`
|
APIKeyAuth APIKeyAuthCacheConfig `mapstructure:"api_key_auth_cache"`
|
||||||
DashboardAgg DashboardAggregationConfig `mapstructure:"dashboard_aggregation"`
|
SubscriptionCache SubscriptionCacheConfig `mapstructure:"subscription_cache"`
|
||||||
UsageCleanup UsageCleanupConfig `mapstructure:"usage_cleanup"`
|
SubscriptionMaintenance SubscriptionMaintenanceConfig `mapstructure:"subscription_maintenance"`
|
||||||
Concurrency ConcurrencyConfig `mapstructure:"concurrency"`
|
Dashboard DashboardCacheConfig `mapstructure:"dashboard_cache"`
|
||||||
TokenRefresh TokenRefreshConfig `mapstructure:"token_refresh"`
|
DashboardAgg DashboardAggregationConfig `mapstructure:"dashboard_aggregation"`
|
||||||
RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
|
UsageCleanup UsageCleanupConfig `mapstructure:"usage_cleanup"`
|
||||||
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
|
Concurrency ConcurrencyConfig `mapstructure:"concurrency"`
|
||||||
Gemini GeminiConfig `mapstructure:"gemini"`
|
TokenRefresh TokenRefreshConfig `mapstructure:"token_refresh"`
|
||||||
Update UpdateConfig `mapstructure:"update"`
|
Sora SoraConfig `mapstructure:"sora"`
|
||||||
|
RunMode string `mapstructure:"run_mode" yaml:"run_mode"`
|
||||||
|
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
|
||||||
|
Gemini GeminiConfig `mapstructure:"gemini"`
|
||||||
|
Update UpdateConfig `mapstructure:"update"`
|
||||||
|
Idempotency IdempotencyConfig `mapstructure:"idempotency"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string `mapstructure:"level"`
|
||||||
|
Format string `mapstructure:"format"`
|
||||||
|
ServiceName string `mapstructure:"service_name"`
|
||||||
|
Environment string `mapstructure:"env"`
|
||||||
|
Caller bool `mapstructure:"caller"`
|
||||||
|
StacktraceLevel string `mapstructure:"stacktrace_level"`
|
||||||
|
Output LogOutputConfig `mapstructure:"output"`
|
||||||
|
Rotation LogRotationConfig `mapstructure:"rotation"`
|
||||||
|
Sampling LogSamplingConfig `mapstructure:"sampling"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogOutputConfig struct {
|
||||||
|
ToStdout bool `mapstructure:"to_stdout"`
|
||||||
|
ToFile bool `mapstructure:"to_file"`
|
||||||
|
FilePath string `mapstructure:"file_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogRotationConfig struct {
|
||||||
|
MaxSizeMB int `mapstructure:"max_size_mb"`
|
||||||
|
MaxBackups int `mapstructure:"max_backups"`
|
||||||
|
MaxAgeDays int `mapstructure:"max_age_days"`
|
||||||
|
Compress bool `mapstructure:"compress"`
|
||||||
|
LocalTime bool `mapstructure:"local_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogSamplingConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
Initial int `mapstructure:"initial"`
|
||||||
|
Thereafter int `mapstructure:"thereafter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GeminiConfig struct {
|
type GeminiConfig struct {
|
||||||
@@ -94,6 +138,25 @@ type UpdateConfig struct {
|
|||||||
ProxyURL string `mapstructure:"proxy_url"`
|
ProxyURL string `mapstructure:"proxy_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IdempotencyConfig struct {
|
||||||
|
// ObserveOnly 为 true 时处于观察期:未携带 Idempotency-Key 的请求继续放行。
|
||||||
|
ObserveOnly bool `mapstructure:"observe_only"`
|
||||||
|
// DefaultTTLSeconds 关键写接口的幂等记录默认 TTL(秒)。
|
||||||
|
DefaultTTLSeconds int `mapstructure:"default_ttl_seconds"`
|
||||||
|
// SystemOperationTTLSeconds 系统操作接口的幂等记录 TTL(秒)。
|
||||||
|
SystemOperationTTLSeconds int `mapstructure:"system_operation_ttl_seconds"`
|
||||||
|
// ProcessingTimeoutSeconds processing 状态锁超时(秒)。
|
||||||
|
ProcessingTimeoutSeconds int `mapstructure:"processing_timeout_seconds"`
|
||||||
|
// FailedRetryBackoffSeconds 失败退避窗口(秒)。
|
||||||
|
FailedRetryBackoffSeconds int `mapstructure:"failed_retry_backoff_seconds"`
|
||||||
|
// MaxStoredResponseLen 持久化响应体最大长度(字节)。
|
||||||
|
MaxStoredResponseLen int `mapstructure:"max_stored_response_len"`
|
||||||
|
// CleanupIntervalSeconds 过期记录清理周期(秒)。
|
||||||
|
CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"`
|
||||||
|
// CleanupBatchSize 每次清理的最大记录数。
|
||||||
|
CleanupBatchSize int `mapstructure:"cleanup_batch_size"`
|
||||||
|
}
|
||||||
|
|
||||||
type LinuxDoConnectConfig struct {
|
type LinuxDoConnectConfig struct {
|
||||||
Enabled bool `mapstructure:"enabled"`
|
Enabled bool `mapstructure:"enabled"`
|
||||||
ClientID string `mapstructure:"client_id"`
|
ClientID string `mapstructure:"client_id"`
|
||||||
@@ -126,6 +189,8 @@ type TokenRefreshConfig struct {
|
|||||||
MaxRetries int `mapstructure:"max_retries"`
|
MaxRetries int `mapstructure:"max_retries"`
|
||||||
// 重试退避基础时间(秒)
|
// 重试退避基础时间(秒)
|
||||||
RetryBackoffSeconds int `mapstructure:"retry_backoff_seconds"`
|
RetryBackoffSeconds int `mapstructure:"retry_backoff_seconds"`
|
||||||
|
// 是否允许 OpenAI 刷新器同步覆盖关联的 Sora 账号 token(默认关闭)
|
||||||
|
SyncLinkedSoraAccounts bool `mapstructure:"sync_linked_sora_accounts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PricingConfig struct {
|
type PricingConfig struct {
|
||||||
@@ -147,6 +212,7 @@ type ServerConfig struct {
|
|||||||
Host string `mapstructure:"host"`
|
Host string `mapstructure:"host"`
|
||||||
Port int `mapstructure:"port"`
|
Port int `mapstructure:"port"`
|
||||||
Mode string `mapstructure:"mode"` // debug/release
|
Mode string `mapstructure:"mode"` // debug/release
|
||||||
|
FrontendURL string `mapstructure:"frontend_url"` // 前端基础 URL,用于生成邮件中的外部链接
|
||||||
ReadHeaderTimeout int `mapstructure:"read_header_timeout"` // 读取请求头超时(秒)
|
ReadHeaderTimeout int `mapstructure:"read_header_timeout"` // 读取请求头超时(秒)
|
||||||
IdleTimeout int `mapstructure:"idle_timeout"` // 空闲连接超时(秒)
|
IdleTimeout int `mapstructure:"idle_timeout"` // 空闲连接超时(秒)
|
||||||
TrustedProxies []string `mapstructure:"trusted_proxies"` // 可信代理列表(CIDR/IP)
|
TrustedProxies []string `mapstructure:"trusted_proxies"` // 可信代理列表(CIDR/IP)
|
||||||
@@ -173,6 +239,7 @@ type SecurityConfig struct {
|
|||||||
URLAllowlist URLAllowlistConfig `mapstructure:"url_allowlist"`
|
URLAllowlist URLAllowlistConfig `mapstructure:"url_allowlist"`
|
||||||
ResponseHeaders ResponseHeaderConfig `mapstructure:"response_headers"`
|
ResponseHeaders ResponseHeaderConfig `mapstructure:"response_headers"`
|
||||||
CSP CSPConfig `mapstructure:"csp"`
|
CSP CSPConfig `mapstructure:"csp"`
|
||||||
|
ProxyFallback ProxyFallbackConfig `mapstructure:"proxy_fallback"`
|
||||||
ProxyProbe ProxyProbeConfig `mapstructure:"proxy_probe"`
|
ProxyProbe ProxyProbeConfig `mapstructure:"proxy_probe"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +264,12 @@ type CSPConfig struct {
|
|||||||
Policy string `mapstructure:"policy"`
|
Policy string `mapstructure:"policy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProxyFallbackConfig struct {
|
||||||
|
// AllowDirectOnError 当代理初始化失败时是否允许回退直连。
|
||||||
|
// 默认 false:避免因代理配置错误导致 IP 泄露/关联。
|
||||||
|
AllowDirectOnError bool `mapstructure:"allow_direct_on_error"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProxyProbeConfig struct {
|
type ProxyProbeConfig struct {
|
||||||
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` // 已禁用:禁止跳过 TLS 证书验证
|
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"` // 已禁用:禁止跳过 TLS 证书验证
|
||||||
}
|
}
|
||||||
@@ -217,6 +290,59 @@ type ConcurrencyConfig struct {
|
|||||||
PingInterval int `mapstructure:"ping_interval"`
|
PingInterval int `mapstructure:"ping_interval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SoraConfig 直连 Sora 配置
|
||||||
|
type SoraConfig struct {
|
||||||
|
Client SoraClientConfig `mapstructure:"client"`
|
||||||
|
Storage SoraStorageConfig `mapstructure:"storage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraClientConfig 直连 Sora 客户端配置
|
||||||
|
type SoraClientConfig struct {
|
||||||
|
BaseURL string `mapstructure:"base_url"`
|
||||||
|
TimeoutSeconds int `mapstructure:"timeout_seconds"`
|
||||||
|
MaxRetries int `mapstructure:"max_retries"`
|
||||||
|
CloudflareChallengeCooldownSeconds int `mapstructure:"cloudflare_challenge_cooldown_seconds"`
|
||||||
|
PollIntervalSeconds int `mapstructure:"poll_interval_seconds"`
|
||||||
|
MaxPollAttempts int `mapstructure:"max_poll_attempts"`
|
||||||
|
RecentTaskLimit int `mapstructure:"recent_task_limit"`
|
||||||
|
RecentTaskLimitMax int `mapstructure:"recent_task_limit_max"`
|
||||||
|
Debug bool `mapstructure:"debug"`
|
||||||
|
UseOpenAITokenProvider bool `mapstructure:"use_openai_token_provider"`
|
||||||
|
Headers map[string]string `mapstructure:"headers"`
|
||||||
|
UserAgent string `mapstructure:"user_agent"`
|
||||||
|
DisableTLSFingerprint bool `mapstructure:"disable_tls_fingerprint"`
|
||||||
|
CurlCFFISidecar SoraCurlCFFISidecarConfig `mapstructure:"curl_cffi_sidecar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraCurlCFFISidecarConfig Sora 专用 curl_cffi sidecar 配置
|
||||||
|
type SoraCurlCFFISidecarConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
BaseURL string `mapstructure:"base_url"`
|
||||||
|
Impersonate string `mapstructure:"impersonate"`
|
||||||
|
TimeoutSeconds int `mapstructure:"timeout_seconds"`
|
||||||
|
SessionReuseEnabled bool `mapstructure:"session_reuse_enabled"`
|
||||||
|
SessionTTLSeconds int `mapstructure:"session_ttl_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraStorageConfig 媒体存储配置
|
||||||
|
type SoraStorageConfig struct {
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
LocalPath string `mapstructure:"local_path"`
|
||||||
|
FallbackToUpstream bool `mapstructure:"fallback_to_upstream"`
|
||||||
|
MaxConcurrentDownloads int `mapstructure:"max_concurrent_downloads"`
|
||||||
|
DownloadTimeoutSeconds int `mapstructure:"download_timeout_seconds"`
|
||||||
|
MaxDownloadBytes int64 `mapstructure:"max_download_bytes"`
|
||||||
|
Debug bool `mapstructure:"debug"`
|
||||||
|
Cleanup SoraStorageCleanupConfig `mapstructure:"cleanup"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraStorageCleanupConfig 媒体清理配置
|
||||||
|
type SoraStorageCleanupConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
Schedule string `mapstructure:"schedule"`
|
||||||
|
RetentionDays int `mapstructure:"retention_days"`
|
||||||
|
}
|
||||||
|
|
||||||
// GatewayConfig API网关相关配置
|
// GatewayConfig API网关相关配置
|
||||||
type GatewayConfig struct {
|
type GatewayConfig struct {
|
||||||
// 等待上游响应头的超时时间(秒),0表示无超时
|
// 等待上游响应头的超时时间(秒),0表示无超时
|
||||||
@@ -224,8 +350,20 @@ type GatewayConfig struct {
|
|||||||
ResponseHeaderTimeout int `mapstructure:"response_header_timeout"`
|
ResponseHeaderTimeout int `mapstructure:"response_header_timeout"`
|
||||||
// 请求体最大字节数,用于网关请求体大小限制
|
// 请求体最大字节数,用于网关请求体大小限制
|
||||||
MaxBodySize int64 `mapstructure:"max_body_size"`
|
MaxBodySize int64 `mapstructure:"max_body_size"`
|
||||||
|
// 非流式上游响应体读取上限(字节),用于防止无界读取导致内存放大
|
||||||
|
UpstreamResponseReadMaxBytes int64 `mapstructure:"upstream_response_read_max_bytes"`
|
||||||
|
// 代理探测响应体读取上限(字节)
|
||||||
|
ProxyProbeResponseReadMaxBytes int64 `mapstructure:"proxy_probe_response_read_max_bytes"`
|
||||||
|
// Gemini 上游响应头调试日志开关(默认关闭,避免高频日志开销)
|
||||||
|
GeminiDebugResponseHeaders bool `mapstructure:"gemini_debug_response_headers"`
|
||||||
// ConnectionPoolIsolation: 上游连接池隔离策略(proxy/account/account_proxy)
|
// ConnectionPoolIsolation: 上游连接池隔离策略(proxy/account/account_proxy)
|
||||||
ConnectionPoolIsolation string `mapstructure:"connection_pool_isolation"`
|
ConnectionPoolIsolation string `mapstructure:"connection_pool_isolation"`
|
||||||
|
// ForceCodexCLI: 强制将 OpenAI `/v1/responses` 请求按 Codex CLI 处理。
|
||||||
|
// 用于网关未透传/改写 User-Agent 时的兼容兜底(默认关闭,避免影响其他客户端)。
|
||||||
|
ForceCodexCLI bool `mapstructure:"force_codex_cli"`
|
||||||
|
// OpenAIPassthroughAllowTimeoutHeaders: OpenAI 透传模式是否放行客户端超时头
|
||||||
|
// 关闭(默认)可避免 x-stainless-timeout 等头导致上游提前断流。
|
||||||
|
OpenAIPassthroughAllowTimeoutHeaders bool `mapstructure:"openai_passthrough_allow_timeout_headers"`
|
||||||
|
|
||||||
// HTTP 上游连接池配置(性能优化:支持高并发场景调优)
|
// HTTP 上游连接池配置(性能优化:支持高并发场景调优)
|
||||||
// MaxIdleConns: 所有主机的最大空闲连接总数
|
// MaxIdleConns: 所有主机的最大空闲连接总数
|
||||||
@@ -271,6 +409,24 @@ type GatewayConfig struct {
|
|||||||
// 是否允许对部分 400 错误触发 failover(默认关闭以避免改变语义)
|
// 是否允许对部分 400 错误触发 failover(默认关闭以避免改变语义)
|
||||||
FailoverOn400 bool `mapstructure:"failover_on_400"`
|
FailoverOn400 bool `mapstructure:"failover_on_400"`
|
||||||
|
|
||||||
|
// Sora 专用配置
|
||||||
|
// SoraMaxBodySize: Sora 请求体最大字节数(0 表示使用 gateway.max_body_size)
|
||||||
|
SoraMaxBodySize int64 `mapstructure:"sora_max_body_size"`
|
||||||
|
// SoraStreamTimeoutSeconds: Sora 流式请求总超时(秒,0 表示不限制)
|
||||||
|
SoraStreamTimeoutSeconds int `mapstructure:"sora_stream_timeout_seconds"`
|
||||||
|
// SoraRequestTimeoutSeconds: Sora 非流式请求超时(秒,0 表示不限制)
|
||||||
|
SoraRequestTimeoutSeconds int `mapstructure:"sora_request_timeout_seconds"`
|
||||||
|
// SoraStreamMode: stream 强制策略(force/error)
|
||||||
|
SoraStreamMode string `mapstructure:"sora_stream_mode"`
|
||||||
|
// SoraModelFilters: 模型列表过滤配置
|
||||||
|
SoraModelFilters SoraModelFiltersConfig `mapstructure:"sora_model_filters"`
|
||||||
|
// SoraMediaRequireAPIKey: 是否要求访问 /sora/media 携带 API Key
|
||||||
|
SoraMediaRequireAPIKey bool `mapstructure:"sora_media_require_api_key"`
|
||||||
|
// SoraMediaSigningKey: /sora/media 临时签名密钥(空表示禁用签名)
|
||||||
|
SoraMediaSigningKey string `mapstructure:"sora_media_signing_key"`
|
||||||
|
// SoraMediaSignedURLTTLSeconds: 临时签名 URL 有效期(秒,<=0 表示禁用)
|
||||||
|
SoraMediaSignedURLTTLSeconds int `mapstructure:"sora_media_signed_url_ttl_seconds"`
|
||||||
|
|
||||||
// 账户切换最大次数(遇到上游错误时切换到其他账户的次数上限)
|
// 账户切换最大次数(遇到上游错误时切换到其他账户的次数上限)
|
||||||
MaxAccountSwitches int `mapstructure:"max_account_switches"`
|
MaxAccountSwitches int `mapstructure:"max_account_switches"`
|
||||||
// Gemini 账户切换最大次数(Gemini 平台单独配置,因 API 限制更严格)
|
// Gemini 账户切换最大次数(Gemini 平台单独配置,因 API 限制更严格)
|
||||||
@@ -284,6 +440,53 @@ type GatewayConfig struct {
|
|||||||
|
|
||||||
// TLSFingerprint: TLS指纹伪装配置
|
// TLSFingerprint: TLS指纹伪装配置
|
||||||
TLSFingerprint TLSFingerprintConfig `mapstructure:"tls_fingerprint"`
|
TLSFingerprint TLSFingerprintConfig `mapstructure:"tls_fingerprint"`
|
||||||
|
|
||||||
|
// UsageRecord: 使用量记录异步队列配置(有界队列 + 固定 worker)
|
||||||
|
UsageRecord GatewayUsageRecordConfig `mapstructure:"usage_record"`
|
||||||
|
|
||||||
|
// UserGroupRateCacheTTLSeconds: 用户分组倍率热路径缓存 TTL(秒)
|
||||||
|
UserGroupRateCacheTTLSeconds int `mapstructure:"user_group_rate_cache_ttl_seconds"`
|
||||||
|
// ModelsListCacheTTLSeconds: /v1/models 模型列表短缓存 TTL(秒)
|
||||||
|
ModelsListCacheTTLSeconds int `mapstructure:"models_list_cache_ttl_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GatewayUsageRecordConfig 使用量记录异步队列配置
|
||||||
|
type GatewayUsageRecordConfig struct {
|
||||||
|
// WorkerCount: worker 初始数量(自动扩缩容开启时作为初始并发上限)
|
||||||
|
WorkerCount int `mapstructure:"worker_count"`
|
||||||
|
// QueueSize: 队列容量(有界)
|
||||||
|
QueueSize int `mapstructure:"queue_size"`
|
||||||
|
// TaskTimeoutSeconds: 单个使用量记录任务超时(秒)
|
||||||
|
TaskTimeoutSeconds int `mapstructure:"task_timeout_seconds"`
|
||||||
|
// OverflowPolicy: 队列满时策略(drop/sample/sync)
|
||||||
|
OverflowPolicy string `mapstructure:"overflow_policy"`
|
||||||
|
// OverflowSamplePercent: sample 策略下,同步回写采样百分比(1-100)
|
||||||
|
OverflowSamplePercent int `mapstructure:"overflow_sample_percent"`
|
||||||
|
|
||||||
|
// AutoScaleEnabled: 是否启用 worker 自动扩缩容
|
||||||
|
AutoScaleEnabled bool `mapstructure:"auto_scale_enabled"`
|
||||||
|
// AutoScaleMinWorkers: 自动扩缩容最小 worker 数
|
||||||
|
AutoScaleMinWorkers int `mapstructure:"auto_scale_min_workers"`
|
||||||
|
// AutoScaleMaxWorkers: 自动扩缩容最大 worker 数
|
||||||
|
AutoScaleMaxWorkers int `mapstructure:"auto_scale_max_workers"`
|
||||||
|
// AutoScaleUpQueuePercent: 队列占用率达到该阈值时触发扩容
|
||||||
|
AutoScaleUpQueuePercent int `mapstructure:"auto_scale_up_queue_percent"`
|
||||||
|
// AutoScaleDownQueuePercent: 队列占用率低于该阈值时触发缩容
|
||||||
|
AutoScaleDownQueuePercent int `mapstructure:"auto_scale_down_queue_percent"`
|
||||||
|
// AutoScaleUpStep: 每次扩容步长
|
||||||
|
AutoScaleUpStep int `mapstructure:"auto_scale_up_step"`
|
||||||
|
// AutoScaleDownStep: 每次缩容步长
|
||||||
|
AutoScaleDownStep int `mapstructure:"auto_scale_down_step"`
|
||||||
|
// AutoScaleCheckIntervalSeconds: 自动扩缩容检测间隔(秒)
|
||||||
|
AutoScaleCheckIntervalSeconds int `mapstructure:"auto_scale_check_interval_seconds"`
|
||||||
|
// AutoScaleCooldownSeconds: 自动扩缩容冷却时间(秒)
|
||||||
|
AutoScaleCooldownSeconds int `mapstructure:"auto_scale_cooldown_seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoraModelFiltersConfig Sora 模型过滤配置
|
||||||
|
type SoraModelFiltersConfig struct {
|
||||||
|
// HidePromptEnhance 是否隐藏 prompt-enhance 模型
|
||||||
|
HidePromptEnhance bool `mapstructure:"hide_prompt_enhance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSFingerprintConfig TLS指纹伪装配置
|
// TLSFingerprintConfig TLS指纹伪装配置
|
||||||
@@ -479,8 +682,9 @@ type OpsMetricsCollectorCacheConfig struct {
|
|||||||
type JWTConfig struct {
|
type JWTConfig struct {
|
||||||
Secret string `mapstructure:"secret"`
|
Secret string `mapstructure:"secret"`
|
||||||
ExpireHour int `mapstructure:"expire_hour"`
|
ExpireHour int `mapstructure:"expire_hour"`
|
||||||
// AccessTokenExpireMinutes: Access Token有效期(分钟),默认15分钟
|
// AccessTokenExpireMinutes: Access Token有效期(分钟)
|
||||||
// 短有效期减少被盗用风险,配合Refresh Token实现无感续期
|
// - >0: 使用分钟配置(优先级高于 ExpireHour)
|
||||||
|
// - =0: 回退使用 ExpireHour(向后兼容旧配置)
|
||||||
AccessTokenExpireMinutes int `mapstructure:"access_token_expire_minutes"`
|
AccessTokenExpireMinutes int `mapstructure:"access_token_expire_minutes"`
|
||||||
// RefreshTokenExpireDays: Refresh Token有效期(天),默认30天
|
// RefreshTokenExpireDays: Refresh Token有效期(天),默认30天
|
||||||
RefreshTokenExpireDays int `mapstructure:"refresh_token_expire_days"`
|
RefreshTokenExpireDays int `mapstructure:"refresh_token_expire_days"`
|
||||||
@@ -525,6 +729,20 @@ type APIKeyAuthCacheConfig struct {
|
|||||||
Singleflight bool `mapstructure:"singleflight"`
|
Singleflight bool `mapstructure:"singleflight"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubscriptionCacheConfig 订阅认证 L1 缓存配置
|
||||||
|
type SubscriptionCacheConfig struct {
|
||||||
|
L1Size int `mapstructure:"l1_size"`
|
||||||
|
L1TTLSeconds int `mapstructure:"l1_ttl_seconds"`
|
||||||
|
JitterPercent int `mapstructure:"jitter_percent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionMaintenanceConfig 订阅窗口维护后台任务配置。
|
||||||
|
// 用于将“请求路径触发的维护动作”有界化,避免高并发下 goroutine 膨胀。
|
||||||
|
type SubscriptionMaintenanceConfig struct {
|
||||||
|
WorkerCount int `mapstructure:"worker_count"`
|
||||||
|
QueueSize int `mapstructure:"queue_size"`
|
||||||
|
}
|
||||||
|
|
||||||
// DashboardCacheConfig 仪表盘统计缓存配置
|
// DashboardCacheConfig 仪表盘统计缓存配置
|
||||||
type DashboardCacheConfig struct {
|
type DashboardCacheConfig struct {
|
||||||
// Enabled: 是否启用仪表盘缓存
|
// Enabled: 是否启用仪表盘缓存
|
||||||
@@ -588,7 +806,19 @@ func NormalizeRunMode(value string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load 读取并校验完整配置(要求 jwt.secret 已显式提供)。
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
|
return load(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadForBootstrap 读取启动阶段配置。
|
||||||
|
//
|
||||||
|
// 启动阶段允许 jwt.secret 先留空,后续由数据库初始化流程补齐并再次完整校验。
|
||||||
|
func LoadForBootstrap() (*Config, error) {
|
||||||
|
return load(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func load(allowMissingJWTSecret bool) (*Config, error) {
|
||||||
viper.SetConfigName("config")
|
viper.SetConfigName("config")
|
||||||
viper.SetConfigType("yaml")
|
viper.SetConfigType("yaml")
|
||||||
|
|
||||||
@@ -630,6 +860,7 @@ func Load() (*Config, error) {
|
|||||||
if cfg.Server.Mode == "" {
|
if cfg.Server.Mode == "" {
|
||||||
cfg.Server.Mode = "debug"
|
cfg.Server.Mode = "debug"
|
||||||
}
|
}
|
||||||
|
cfg.Server.FrontendURL = strings.TrimSpace(cfg.Server.FrontendURL)
|
||||||
cfg.JWT.Secret = strings.TrimSpace(cfg.JWT.Secret)
|
cfg.JWT.Secret = strings.TrimSpace(cfg.JWT.Secret)
|
||||||
cfg.LinuxDo.ClientID = strings.TrimSpace(cfg.LinuxDo.ClientID)
|
cfg.LinuxDo.ClientID = strings.TrimSpace(cfg.LinuxDo.ClientID)
|
||||||
cfg.LinuxDo.ClientSecret = strings.TrimSpace(cfg.LinuxDo.ClientSecret)
|
cfg.LinuxDo.ClientSecret = strings.TrimSpace(cfg.LinuxDo.ClientSecret)
|
||||||
@@ -648,15 +879,12 @@ func Load() (*Config, error) {
|
|||||||
cfg.Security.ResponseHeaders.AdditionalAllowed = normalizeStringSlice(cfg.Security.ResponseHeaders.AdditionalAllowed)
|
cfg.Security.ResponseHeaders.AdditionalAllowed = normalizeStringSlice(cfg.Security.ResponseHeaders.AdditionalAllowed)
|
||||||
cfg.Security.ResponseHeaders.ForceRemove = normalizeStringSlice(cfg.Security.ResponseHeaders.ForceRemove)
|
cfg.Security.ResponseHeaders.ForceRemove = normalizeStringSlice(cfg.Security.ResponseHeaders.ForceRemove)
|
||||||
cfg.Security.CSP.Policy = strings.TrimSpace(cfg.Security.CSP.Policy)
|
cfg.Security.CSP.Policy = strings.TrimSpace(cfg.Security.CSP.Policy)
|
||||||
|
cfg.Log.Level = strings.ToLower(strings.TrimSpace(cfg.Log.Level))
|
||||||
if cfg.JWT.Secret == "" {
|
cfg.Log.Format = strings.ToLower(strings.TrimSpace(cfg.Log.Format))
|
||||||
secret, err := generateJWTSecret(64)
|
cfg.Log.ServiceName = strings.TrimSpace(cfg.Log.ServiceName)
|
||||||
if err != nil {
|
cfg.Log.Environment = strings.TrimSpace(cfg.Log.Environment)
|
||||||
return nil, fmt.Errorf("generate jwt secret error: %w", err)
|
cfg.Log.StacktraceLevel = strings.ToLower(strings.TrimSpace(cfg.Log.StacktraceLevel))
|
||||||
}
|
cfg.Log.Output.FilePath = strings.TrimSpace(cfg.Log.Output.FilePath)
|
||||||
cfg.JWT.Secret = secret
|
|
||||||
log.Println("Warning: JWT secret auto-generated. Consider setting a fixed secret for production.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-generate TOTP encryption key if not set (32 bytes = 64 hex chars for AES-256)
|
// Auto-generate TOTP encryption key if not set (32 bytes = 64 hex chars for AES-256)
|
||||||
cfg.Totp.EncryptionKey = strings.TrimSpace(cfg.Totp.EncryptionKey)
|
cfg.Totp.EncryptionKey = strings.TrimSpace(cfg.Totp.EncryptionKey)
|
||||||
@@ -667,29 +895,39 @@ func Load() (*Config, error) {
|
|||||||
}
|
}
|
||||||
cfg.Totp.EncryptionKey = key
|
cfg.Totp.EncryptionKey = key
|
||||||
cfg.Totp.EncryptionKeyConfigured = false
|
cfg.Totp.EncryptionKeyConfigured = false
|
||||||
log.Println("Warning: TOTP encryption key auto-generated. Consider setting a fixed key for production.")
|
slog.Warn("TOTP encryption key auto-generated. Consider setting a fixed key for production.")
|
||||||
} else {
|
} else {
|
||||||
cfg.Totp.EncryptionKeyConfigured = true
|
cfg.Totp.EncryptionKeyConfigured = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
originalJWTSecret := cfg.JWT.Secret
|
||||||
|
if allowMissingJWTSecret && originalJWTSecret == "" {
|
||||||
|
// 启动阶段允许先无 JWT 密钥,后续在数据库初始化后补齐。
|
||||||
|
cfg.JWT.Secret = strings.Repeat("0", 32)
|
||||||
|
}
|
||||||
|
|
||||||
if err := cfg.Validate(); err != nil {
|
if err := cfg.Validate(); err != nil {
|
||||||
return nil, fmt.Errorf("validate config error: %w", err)
|
return nil, fmt.Errorf("validate config error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if allowMissingJWTSecret && originalJWTSecret == "" {
|
||||||
|
cfg.JWT.Secret = ""
|
||||||
|
}
|
||||||
|
|
||||||
if !cfg.Security.URLAllowlist.Enabled {
|
if !cfg.Security.URLAllowlist.Enabled {
|
||||||
log.Println("Warning: security.url_allowlist.enabled=false; allowlist/SSRF checks disabled (minimal format validation only).")
|
slog.Warn("security.url_allowlist.enabled=false; allowlist/SSRF checks disabled (minimal format validation only).")
|
||||||
}
|
}
|
||||||
if !cfg.Security.ResponseHeaders.Enabled {
|
if !cfg.Security.ResponseHeaders.Enabled {
|
||||||
log.Println("Warning: security.response_headers.enabled=false; configurable header filtering disabled (default allowlist only).")
|
slog.Warn("security.response_headers.enabled=false; configurable header filtering disabled (default allowlist only).")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.JWT.Secret != "" && isWeakJWTSecret(cfg.JWT.Secret) {
|
if cfg.JWT.Secret != "" && isWeakJWTSecret(cfg.JWT.Secret) {
|
||||||
log.Println("Warning: JWT secret appears weak; use a 32+ character random secret in production.")
|
slog.Warn("JWT secret appears weak; use a 32+ character random secret in production.")
|
||||||
}
|
}
|
||||||
if len(cfg.Security.ResponseHeaders.AdditionalAllowed) > 0 || len(cfg.Security.ResponseHeaders.ForceRemove) > 0 {
|
if len(cfg.Security.ResponseHeaders.AdditionalAllowed) > 0 || len(cfg.Security.ResponseHeaders.ForceRemove) > 0 {
|
||||||
log.Printf("AUDIT: response header policy configured additional_allowed=%v force_remove=%v",
|
slog.Info("response header policy configured",
|
||||||
cfg.Security.ResponseHeaders.AdditionalAllowed,
|
"additional_allowed", cfg.Security.ResponseHeaders.AdditionalAllowed,
|
||||||
cfg.Security.ResponseHeaders.ForceRemove,
|
"force_remove", cfg.Security.ResponseHeaders.ForceRemove,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,7 +940,8 @@ func setDefaults() {
|
|||||||
// Server
|
// Server
|
||||||
viper.SetDefault("server.host", "0.0.0.0")
|
viper.SetDefault("server.host", "0.0.0.0")
|
||||||
viper.SetDefault("server.port", 8080)
|
viper.SetDefault("server.port", 8080)
|
||||||
viper.SetDefault("server.mode", "debug")
|
viper.SetDefault("server.mode", "release")
|
||||||
|
viper.SetDefault("server.frontend_url", "")
|
||||||
viper.SetDefault("server.read_header_timeout", 30) // 30秒读取请求头
|
viper.SetDefault("server.read_header_timeout", 30) // 30秒读取请求头
|
||||||
viper.SetDefault("server.idle_timeout", 120) // 120秒空闲超时
|
viper.SetDefault("server.idle_timeout", 120) // 120秒空闲超时
|
||||||
viper.SetDefault("server.trusted_proxies", []string{})
|
viper.SetDefault("server.trusted_proxies", []string{})
|
||||||
@@ -715,6 +954,25 @@ func setDefaults() {
|
|||||||
viper.SetDefault("server.h2c.max_upload_buffer_per_connection", 2<<20) // 2MB
|
viper.SetDefault("server.h2c.max_upload_buffer_per_connection", 2<<20) // 2MB
|
||||||
viper.SetDefault("server.h2c.max_upload_buffer_per_stream", 512<<10) // 512KB
|
viper.SetDefault("server.h2c.max_upload_buffer_per_stream", 512<<10) // 512KB
|
||||||
|
|
||||||
|
// Log
|
||||||
|
viper.SetDefault("log.level", "info")
|
||||||
|
viper.SetDefault("log.format", "console")
|
||||||
|
viper.SetDefault("log.service_name", "sub2api")
|
||||||
|
viper.SetDefault("log.env", "production")
|
||||||
|
viper.SetDefault("log.caller", true)
|
||||||
|
viper.SetDefault("log.stacktrace_level", "error")
|
||||||
|
viper.SetDefault("log.output.to_stdout", true)
|
||||||
|
viper.SetDefault("log.output.to_file", true)
|
||||||
|
viper.SetDefault("log.output.file_path", "")
|
||||||
|
viper.SetDefault("log.rotation.max_size_mb", 100)
|
||||||
|
viper.SetDefault("log.rotation.max_backups", 10)
|
||||||
|
viper.SetDefault("log.rotation.max_age_days", 7)
|
||||||
|
viper.SetDefault("log.rotation.compress", true)
|
||||||
|
viper.SetDefault("log.rotation.local_time", true)
|
||||||
|
viper.SetDefault("log.sampling.enabled", false)
|
||||||
|
viper.SetDefault("log.sampling.initial", 100)
|
||||||
|
viper.SetDefault("log.sampling.thereafter", 100)
|
||||||
|
|
||||||
// CORS
|
// CORS
|
||||||
viper.SetDefault("cors.allowed_origins", []string{})
|
viper.SetDefault("cors.allowed_origins", []string{})
|
||||||
viper.SetDefault("cors.allow_credentials", true)
|
viper.SetDefault("cors.allow_credentials", true)
|
||||||
@@ -737,7 +995,7 @@ func setDefaults() {
|
|||||||
viper.SetDefault("security.url_allowlist.crs_hosts", []string{})
|
viper.SetDefault("security.url_allowlist.crs_hosts", []string{})
|
||||||
viper.SetDefault("security.url_allowlist.allow_private_hosts", true)
|
viper.SetDefault("security.url_allowlist.allow_private_hosts", true)
|
||||||
viper.SetDefault("security.url_allowlist.allow_insecure_http", true)
|
viper.SetDefault("security.url_allowlist.allow_insecure_http", true)
|
||||||
viper.SetDefault("security.response_headers.enabled", false)
|
viper.SetDefault("security.response_headers.enabled", true)
|
||||||
viper.SetDefault("security.response_headers.additional_allowed", []string{})
|
viper.SetDefault("security.response_headers.additional_allowed", []string{})
|
||||||
viper.SetDefault("security.response_headers.force_remove", []string{})
|
viper.SetDefault("security.response_headers.force_remove", []string{})
|
||||||
viper.SetDefault("security.csp.enabled", true)
|
viper.SetDefault("security.csp.enabled", true)
|
||||||
@@ -775,9 +1033,9 @@ func setDefaults() {
|
|||||||
viper.SetDefault("database.user", "postgres")
|
viper.SetDefault("database.user", "postgres")
|
||||||
viper.SetDefault("database.password", "postgres")
|
viper.SetDefault("database.password", "postgres")
|
||||||
viper.SetDefault("database.dbname", "sub2api")
|
viper.SetDefault("database.dbname", "sub2api")
|
||||||
viper.SetDefault("database.sslmode", "disable")
|
viper.SetDefault("database.sslmode", "prefer")
|
||||||
viper.SetDefault("database.max_open_conns", 50)
|
viper.SetDefault("database.max_open_conns", 256)
|
||||||
viper.SetDefault("database.max_idle_conns", 10)
|
viper.SetDefault("database.max_idle_conns", 128)
|
||||||
viper.SetDefault("database.conn_max_lifetime_minutes", 30)
|
viper.SetDefault("database.conn_max_lifetime_minutes", 30)
|
||||||
viper.SetDefault("database.conn_max_idle_time_minutes", 5)
|
viper.SetDefault("database.conn_max_idle_time_minutes", 5)
|
||||||
|
|
||||||
@@ -789,8 +1047,8 @@ func setDefaults() {
|
|||||||
viper.SetDefault("redis.dial_timeout_seconds", 5)
|
viper.SetDefault("redis.dial_timeout_seconds", 5)
|
||||||
viper.SetDefault("redis.read_timeout_seconds", 3)
|
viper.SetDefault("redis.read_timeout_seconds", 3)
|
||||||
viper.SetDefault("redis.write_timeout_seconds", 3)
|
viper.SetDefault("redis.write_timeout_seconds", 3)
|
||||||
viper.SetDefault("redis.pool_size", 128)
|
viper.SetDefault("redis.pool_size", 1024)
|
||||||
viper.SetDefault("redis.min_idle_conns", 10)
|
viper.SetDefault("redis.min_idle_conns", 128)
|
||||||
viper.SetDefault("redis.enable_tls", false)
|
viper.SetDefault("redis.enable_tls", false)
|
||||||
|
|
||||||
// Ops (vNext)
|
// Ops (vNext)
|
||||||
@@ -810,9 +1068,9 @@ func setDefaults() {
|
|||||||
// JWT
|
// JWT
|
||||||
viper.SetDefault("jwt.secret", "")
|
viper.SetDefault("jwt.secret", "")
|
||||||
viper.SetDefault("jwt.expire_hour", 24)
|
viper.SetDefault("jwt.expire_hour", 24)
|
||||||
viper.SetDefault("jwt.access_token_expire_minutes", 360) // 6小时Access Token有效期
|
viper.SetDefault("jwt.access_token_expire_minutes", 0) // 0 表示回退到 expire_hour
|
||||||
viper.SetDefault("jwt.refresh_token_expire_days", 30) // 30天Refresh Token有效期
|
viper.SetDefault("jwt.refresh_token_expire_days", 30) // 30天Refresh Token有效期
|
||||||
viper.SetDefault("jwt.refresh_window_minutes", 2) // 过期前2分钟开始允许刷新
|
viper.SetDefault("jwt.refresh_window_minutes", 2) // 过期前2分钟开始允许刷新
|
||||||
|
|
||||||
// TOTP
|
// TOTP
|
||||||
viper.SetDefault("totp.encryption_key", "")
|
viper.SetDefault("totp.encryption_key", "")
|
||||||
@@ -830,9 +1088,9 @@ func setDefaults() {
|
|||||||
// RateLimit
|
// RateLimit
|
||||||
viper.SetDefault("rate_limit.overload_cooldown_minutes", 10)
|
viper.SetDefault("rate_limit.overload_cooldown_minutes", 10)
|
||||||
|
|
||||||
// Pricing - 从 price-mirror 分支同步,该分支维护了 sha256 哈希文件用于增量更新检查
|
// Pricing - 从 model-price-repo 同步模型定价和上下文窗口数据的配置
|
||||||
viper.SetDefault("pricing.remote_url", "https://raw.githubusercontent.com/Wei-Shaw/claude-relay-service/price-mirror/model_prices_and_context_window.json")
|
viper.SetDefault("pricing.remote_url", "https://github.com/Wei-Shaw/model-price-repo/raw/refs/heads/main/model_prices_and_context_window.json")
|
||||||
viper.SetDefault("pricing.hash_url", "https://raw.githubusercontent.com/Wei-Shaw/claude-relay-service/price-mirror/model_prices_and_context_window.sha256")
|
viper.SetDefault("pricing.hash_url", "https://github.com/Wei-Shaw/model-price-repo/raw/refs/heads/main/model_prices_and_context_window.sha256")
|
||||||
viper.SetDefault("pricing.data_dir", "./data")
|
viper.SetDefault("pricing.data_dir", "./data")
|
||||||
viper.SetDefault("pricing.fallback_file", "./resources/model-pricing/model_prices_and_context_window.json")
|
viper.SetDefault("pricing.fallback_file", "./resources/model-pricing/model_prices_and_context_window.json")
|
||||||
viper.SetDefault("pricing.update_interval_hours", 24)
|
viper.SetDefault("pricing.update_interval_hours", 24)
|
||||||
@@ -849,6 +1107,11 @@ func setDefaults() {
|
|||||||
viper.SetDefault("api_key_auth_cache.jitter_percent", 10)
|
viper.SetDefault("api_key_auth_cache.jitter_percent", 10)
|
||||||
viper.SetDefault("api_key_auth_cache.singleflight", true)
|
viper.SetDefault("api_key_auth_cache.singleflight", true)
|
||||||
|
|
||||||
|
// Subscription auth L1 cache
|
||||||
|
viper.SetDefault("subscription_cache.l1_size", 16384)
|
||||||
|
viper.SetDefault("subscription_cache.l1_ttl_seconds", 10)
|
||||||
|
viper.SetDefault("subscription_cache.jitter_percent", 10)
|
||||||
|
|
||||||
// Dashboard cache
|
// Dashboard cache
|
||||||
viper.SetDefault("dashboard_cache.enabled", true)
|
viper.SetDefault("dashboard_cache.enabled", true)
|
||||||
viper.SetDefault("dashboard_cache.key_prefix", "sub2api:")
|
viper.SetDefault("dashboard_cache.key_prefix", "sub2api:")
|
||||||
@@ -874,6 +1137,16 @@ func setDefaults() {
|
|||||||
viper.SetDefault("usage_cleanup.worker_interval_seconds", 10)
|
viper.SetDefault("usage_cleanup.worker_interval_seconds", 10)
|
||||||
viper.SetDefault("usage_cleanup.task_timeout_seconds", 1800)
|
viper.SetDefault("usage_cleanup.task_timeout_seconds", 1800)
|
||||||
|
|
||||||
|
// Idempotency
|
||||||
|
viper.SetDefault("idempotency.observe_only", true)
|
||||||
|
viper.SetDefault("idempotency.default_ttl_seconds", 86400)
|
||||||
|
viper.SetDefault("idempotency.system_operation_ttl_seconds", 3600)
|
||||||
|
viper.SetDefault("idempotency.processing_timeout_seconds", 30)
|
||||||
|
viper.SetDefault("idempotency.failed_retry_backoff_seconds", 5)
|
||||||
|
viper.SetDefault("idempotency.max_stored_response_len", 64*1024)
|
||||||
|
viper.SetDefault("idempotency.cleanup_interval_seconds", 60)
|
||||||
|
viper.SetDefault("idempotency.cleanup_batch_size", 500)
|
||||||
|
|
||||||
// Gateway
|
// Gateway
|
||||||
viper.SetDefault("gateway.response_header_timeout", 600) // 600秒(10分钟)等待上游响应头,LLM高负载时可能排队较久
|
viper.SetDefault("gateway.response_header_timeout", 600) // 600秒(10分钟)等待上游响应头,LLM高负载时可能排队较久
|
||||||
viper.SetDefault("gateway.log_upstream_error_body", true)
|
viper.SetDefault("gateway.log_upstream_error_body", true)
|
||||||
@@ -882,13 +1155,26 @@ func setDefaults() {
|
|||||||
viper.SetDefault("gateway.failover_on_400", false)
|
viper.SetDefault("gateway.failover_on_400", false)
|
||||||
viper.SetDefault("gateway.max_account_switches", 10)
|
viper.SetDefault("gateway.max_account_switches", 10)
|
||||||
viper.SetDefault("gateway.max_account_switches_gemini", 3)
|
viper.SetDefault("gateway.max_account_switches_gemini", 3)
|
||||||
|
viper.SetDefault("gateway.force_codex_cli", false)
|
||||||
|
viper.SetDefault("gateway.openai_passthrough_allow_timeout_headers", false)
|
||||||
viper.SetDefault("gateway.antigravity_fallback_cooldown_minutes", 1)
|
viper.SetDefault("gateway.antigravity_fallback_cooldown_minutes", 1)
|
||||||
|
viper.SetDefault("gateway.antigravity_extra_retries", 10)
|
||||||
viper.SetDefault("gateway.max_body_size", int64(100*1024*1024))
|
viper.SetDefault("gateway.max_body_size", int64(100*1024*1024))
|
||||||
|
viper.SetDefault("gateway.upstream_response_read_max_bytes", int64(8*1024*1024))
|
||||||
|
viper.SetDefault("gateway.proxy_probe_response_read_max_bytes", int64(1024*1024))
|
||||||
|
viper.SetDefault("gateway.gemini_debug_response_headers", false)
|
||||||
|
viper.SetDefault("gateway.sora_max_body_size", int64(256*1024*1024))
|
||||||
|
viper.SetDefault("gateway.sora_stream_timeout_seconds", 900)
|
||||||
|
viper.SetDefault("gateway.sora_request_timeout_seconds", 180)
|
||||||
|
viper.SetDefault("gateway.sora_stream_mode", "force")
|
||||||
|
viper.SetDefault("gateway.sora_model_filters.hide_prompt_enhance", true)
|
||||||
|
viper.SetDefault("gateway.sora_media_require_api_key", true)
|
||||||
|
viper.SetDefault("gateway.sora_media_signed_url_ttl_seconds", 900)
|
||||||
viper.SetDefault("gateway.connection_pool_isolation", ConnectionPoolIsolationAccountProxy)
|
viper.SetDefault("gateway.connection_pool_isolation", ConnectionPoolIsolationAccountProxy)
|
||||||
// HTTP 上游连接池配置(针对 5000+ 并发用户优化)
|
// HTTP 上游连接池配置(针对 5000+ 并发用户优化)
|
||||||
viper.SetDefault("gateway.max_idle_conns", 240) // 最大空闲连接总数(HTTP/2 场景默认)
|
viper.SetDefault("gateway.max_idle_conns", 2560) // 最大空闲连接总数(高并发场景可调大)
|
||||||
viper.SetDefault("gateway.max_idle_conns_per_host", 120) // 每主机最大空闲连接(HTTP/2 场景默认)
|
viper.SetDefault("gateway.max_idle_conns_per_host", 120) // 每主机最大空闲连接(HTTP/2 场景默认)
|
||||||
viper.SetDefault("gateway.max_conns_per_host", 240) // 每主机最大连接数(含活跃,HTTP/2 场景默认)
|
viper.SetDefault("gateway.max_conns_per_host", 1024) // 每主机最大连接数(含活跃;流式/HTTP1.1 场景可调大,如 2400+)
|
||||||
viper.SetDefault("gateway.idle_conn_timeout_seconds", 90) // 空闲连接超时(秒)
|
viper.SetDefault("gateway.idle_conn_timeout_seconds", 90) // 空闲连接超时(秒)
|
||||||
viper.SetDefault("gateway.max_upstream_clients", 5000)
|
viper.SetDefault("gateway.max_upstream_clients", 5000)
|
||||||
viper.SetDefault("gateway.client_idle_ttl_seconds", 900)
|
viper.SetDefault("gateway.client_idle_ttl_seconds", 900)
|
||||||
@@ -912,16 +1198,65 @@ func setDefaults() {
|
|||||||
viper.SetDefault("gateway.scheduling.outbox_lag_rebuild_failures", 3)
|
viper.SetDefault("gateway.scheduling.outbox_lag_rebuild_failures", 3)
|
||||||
viper.SetDefault("gateway.scheduling.outbox_backlog_rebuild_rows", 10000)
|
viper.SetDefault("gateway.scheduling.outbox_backlog_rebuild_rows", 10000)
|
||||||
viper.SetDefault("gateway.scheduling.full_rebuild_interval_seconds", 300)
|
viper.SetDefault("gateway.scheduling.full_rebuild_interval_seconds", 300)
|
||||||
|
viper.SetDefault("gateway.usage_record.worker_count", 128)
|
||||||
|
viper.SetDefault("gateway.usage_record.queue_size", 16384)
|
||||||
|
viper.SetDefault("gateway.usage_record.task_timeout_seconds", 5)
|
||||||
|
viper.SetDefault("gateway.usage_record.overflow_policy", UsageRecordOverflowPolicySample)
|
||||||
|
viper.SetDefault("gateway.usage_record.overflow_sample_percent", 10)
|
||||||
|
viper.SetDefault("gateway.usage_record.auto_scale_enabled", true)
|
||||||
|
viper.SetDefault("gateway.usage_record.auto_scale_min_workers", 128)
|
||||||
|
viper.SetDefault("gateway.usage_record.auto_scale_max_workers", 512)
|
||||||
|
viper.SetDefault("gateway.usage_record.auto_scale_up_queue_percent", 70)
|
||||||
|
viper.SetDefault("gateway.usage_record.auto_scale_down_queue_percent", 15)
|
||||||
|
viper.SetDefault("gateway.usage_record.auto_scale_up_step", 32)
|
||||||
|
viper.SetDefault("gateway.usage_record.auto_scale_down_step", 16)
|
||||||
|
viper.SetDefault("gateway.usage_record.auto_scale_check_interval_seconds", 3)
|
||||||
|
viper.SetDefault("gateway.usage_record.auto_scale_cooldown_seconds", 10)
|
||||||
|
viper.SetDefault("gateway.user_group_rate_cache_ttl_seconds", 30)
|
||||||
|
viper.SetDefault("gateway.models_list_cache_ttl_seconds", 15)
|
||||||
// TLS指纹伪装配置(默认关闭,需要账号级别单独启用)
|
// TLS指纹伪装配置(默认关闭,需要账号级别单独启用)
|
||||||
viper.SetDefault("gateway.tls_fingerprint.enabled", true)
|
viper.SetDefault("gateway.tls_fingerprint.enabled", true)
|
||||||
viper.SetDefault("concurrency.ping_interval", 10)
|
viper.SetDefault("concurrency.ping_interval", 10)
|
||||||
|
|
||||||
|
// Sora 直连配置
|
||||||
|
viper.SetDefault("sora.client.base_url", "https://sora.chatgpt.com/backend")
|
||||||
|
viper.SetDefault("sora.client.timeout_seconds", 120)
|
||||||
|
viper.SetDefault("sora.client.max_retries", 3)
|
||||||
|
viper.SetDefault("sora.client.cloudflare_challenge_cooldown_seconds", 900)
|
||||||
|
viper.SetDefault("sora.client.poll_interval_seconds", 2)
|
||||||
|
viper.SetDefault("sora.client.max_poll_attempts", 600)
|
||||||
|
viper.SetDefault("sora.client.recent_task_limit", 50)
|
||||||
|
viper.SetDefault("sora.client.recent_task_limit_max", 200)
|
||||||
|
viper.SetDefault("sora.client.debug", false)
|
||||||
|
viper.SetDefault("sora.client.use_openai_token_provider", false)
|
||||||
|
viper.SetDefault("sora.client.headers", map[string]string{})
|
||||||
|
viper.SetDefault("sora.client.user_agent", "Sora/1.2026.007 (Android 15; 24122RKC7C; build 2600700)")
|
||||||
|
viper.SetDefault("sora.client.disable_tls_fingerprint", false)
|
||||||
|
viper.SetDefault("sora.client.curl_cffi_sidecar.enabled", true)
|
||||||
|
viper.SetDefault("sora.client.curl_cffi_sidecar.base_url", "http://sora-curl-cffi-sidecar:8080")
|
||||||
|
viper.SetDefault("sora.client.curl_cffi_sidecar.impersonate", "chrome131")
|
||||||
|
viper.SetDefault("sora.client.curl_cffi_sidecar.timeout_seconds", 60)
|
||||||
|
viper.SetDefault("sora.client.curl_cffi_sidecar.session_reuse_enabled", true)
|
||||||
|
viper.SetDefault("sora.client.curl_cffi_sidecar.session_ttl_seconds", 3600)
|
||||||
|
|
||||||
|
viper.SetDefault("sora.storage.type", "local")
|
||||||
|
viper.SetDefault("sora.storage.local_path", "")
|
||||||
|
viper.SetDefault("sora.storage.fallback_to_upstream", true)
|
||||||
|
viper.SetDefault("sora.storage.max_concurrent_downloads", 4)
|
||||||
|
viper.SetDefault("sora.storage.download_timeout_seconds", 120)
|
||||||
|
viper.SetDefault("sora.storage.max_download_bytes", int64(200<<20))
|
||||||
|
viper.SetDefault("sora.storage.debug", false)
|
||||||
|
viper.SetDefault("sora.storage.cleanup.enabled", true)
|
||||||
|
viper.SetDefault("sora.storage.cleanup.retention_days", 7)
|
||||||
|
viper.SetDefault("sora.storage.cleanup.schedule", "0 3 * * *")
|
||||||
|
|
||||||
// TokenRefresh
|
// TokenRefresh
|
||||||
viper.SetDefault("token_refresh.enabled", true)
|
viper.SetDefault("token_refresh.enabled", true)
|
||||||
viper.SetDefault("token_refresh.check_interval_minutes", 5) // 每5分钟检查一次
|
viper.SetDefault("token_refresh.check_interval_minutes", 5) // 每5分钟检查一次
|
||||||
viper.SetDefault("token_refresh.refresh_before_expiry_hours", 0.5) // 提前30分钟刷新(适配Google 1小时token)
|
viper.SetDefault("token_refresh.refresh_before_expiry_hours", 0.5) // 提前30分钟刷新(适配Google 1小时token)
|
||||||
viper.SetDefault("token_refresh.max_retries", 3) // 最多重试3次
|
viper.SetDefault("token_refresh.max_retries", 3) // 最多重试3次
|
||||||
viper.SetDefault("token_refresh.retry_backoff_seconds", 2) // 重试退避基础2秒
|
viper.SetDefault("token_refresh.retry_backoff_seconds", 2) // 重试退避基础2秒
|
||||||
|
viper.SetDefault("token_refresh.sync_linked_sora_accounts", false) // 默认不跨平台覆盖 Sora token
|
||||||
|
|
||||||
// Gemini OAuth - configure via environment variables or config file
|
// Gemini OAuth - configure via environment variables or config file
|
||||||
// GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET
|
// GEMINI_OAUTH_CLIENT_ID and GEMINI_OAUTH_CLIENT_SECRET
|
||||||
@@ -930,9 +1265,106 @@ func setDefaults() {
|
|||||||
viper.SetDefault("gemini.oauth.client_secret", "")
|
viper.SetDefault("gemini.oauth.client_secret", "")
|
||||||
viper.SetDefault("gemini.oauth.scopes", "")
|
viper.SetDefault("gemini.oauth.scopes", "")
|
||||||
viper.SetDefault("gemini.quota.policy", "")
|
viper.SetDefault("gemini.quota.policy", "")
|
||||||
|
|
||||||
|
// Security - proxy fallback
|
||||||
|
viper.SetDefault("security.proxy_fallback.allow_direct_on_error", false)
|
||||||
|
|
||||||
|
// Subscription Maintenance (bounded queue + worker pool)
|
||||||
|
viper.SetDefault("subscription_maintenance.worker_count", 2)
|
||||||
|
viper.SetDefault("subscription_maintenance.queue_size", 1024)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Validate() error {
|
func (c *Config) Validate() error {
|
||||||
|
jwtSecret := strings.TrimSpace(c.JWT.Secret)
|
||||||
|
if jwtSecret == "" {
|
||||||
|
return fmt.Errorf("jwt.secret is required")
|
||||||
|
}
|
||||||
|
// NOTE: 按 UTF-8 编码后的字节长度计算。
|
||||||
|
// 选择 bytes 而不是 rune 计数,确保二进制/随机串的长度语义更接近“熵”而非“字符数”。
|
||||||
|
if len([]byte(jwtSecret)) < 32 {
|
||||||
|
return fmt.Errorf("jwt.secret must be at least 32 bytes")
|
||||||
|
}
|
||||||
|
switch c.Log.Level {
|
||||||
|
case "debug", "info", "warn", "error":
|
||||||
|
case "":
|
||||||
|
return fmt.Errorf("log.level is required")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("log.level must be one of: debug/info/warn/error")
|
||||||
|
}
|
||||||
|
switch c.Log.Format {
|
||||||
|
case "json", "console":
|
||||||
|
case "":
|
||||||
|
return fmt.Errorf("log.format is required")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("log.format must be one of: json/console")
|
||||||
|
}
|
||||||
|
switch c.Log.StacktraceLevel {
|
||||||
|
case "none", "error", "fatal":
|
||||||
|
case "":
|
||||||
|
return fmt.Errorf("log.stacktrace_level is required")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("log.stacktrace_level must be one of: none/error/fatal")
|
||||||
|
}
|
||||||
|
if !c.Log.Output.ToStdout && !c.Log.Output.ToFile {
|
||||||
|
return fmt.Errorf("log.output.to_stdout and log.output.to_file cannot both be false")
|
||||||
|
}
|
||||||
|
if c.Log.Rotation.MaxSizeMB <= 0 {
|
||||||
|
return fmt.Errorf("log.rotation.max_size_mb must be positive")
|
||||||
|
}
|
||||||
|
if c.Log.Rotation.MaxBackups < 0 {
|
||||||
|
return fmt.Errorf("log.rotation.max_backups must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Log.Rotation.MaxAgeDays < 0 {
|
||||||
|
return fmt.Errorf("log.rotation.max_age_days must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Log.Sampling.Enabled {
|
||||||
|
if c.Log.Sampling.Initial <= 0 {
|
||||||
|
return fmt.Errorf("log.sampling.initial must be positive when sampling is enabled")
|
||||||
|
}
|
||||||
|
if c.Log.Sampling.Thereafter <= 0 {
|
||||||
|
return fmt.Errorf("log.sampling.thereafter must be positive when sampling is enabled")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if c.Log.Sampling.Initial < 0 {
|
||||||
|
return fmt.Errorf("log.sampling.initial must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Log.Sampling.Thereafter < 0 {
|
||||||
|
return fmt.Errorf("log.sampling.thereafter must be non-negative")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.SubscriptionMaintenance.WorkerCount < 0 {
|
||||||
|
return fmt.Errorf("subscription_maintenance.worker_count must be non-negative")
|
||||||
|
}
|
||||||
|
if c.SubscriptionMaintenance.QueueSize < 0 {
|
||||||
|
return fmt.Errorf("subscription_maintenance.queue_size must be non-negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gemini OAuth 配置校验:client_id 与 client_secret 必须同时设置或同时留空。
|
||||||
|
// 留空时表示使用内置的 Gemini CLI OAuth 客户端(其 client_secret 通过环境变量注入)。
|
||||||
|
geminiClientID := strings.TrimSpace(c.Gemini.OAuth.ClientID)
|
||||||
|
geminiClientSecret := strings.TrimSpace(c.Gemini.OAuth.ClientSecret)
|
||||||
|
if (geminiClientID == "") != (geminiClientSecret == "") {
|
||||||
|
return fmt.Errorf("gemini.oauth.client_id and gemini.oauth.client_secret must be both set or both empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(c.Server.FrontendURL) != "" {
|
||||||
|
if err := ValidateAbsoluteHTTPURL(c.Server.FrontendURL); err != nil {
|
||||||
|
return fmt.Errorf("server.frontend_url invalid: %w", err)
|
||||||
|
}
|
||||||
|
u, err := url.Parse(strings.TrimSpace(c.Server.FrontendURL))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("server.frontend_url invalid: %w", err)
|
||||||
|
}
|
||||||
|
if u.RawQuery != "" || u.ForceQuery {
|
||||||
|
return fmt.Errorf("server.frontend_url invalid: must not include query")
|
||||||
|
}
|
||||||
|
if u.User != nil {
|
||||||
|
return fmt.Errorf("server.frontend_url invalid: must not include userinfo")
|
||||||
|
}
|
||||||
|
warnIfInsecureURL("server.frontend_url", c.Server.FrontendURL)
|
||||||
|
}
|
||||||
if c.JWT.ExpireHour <= 0 {
|
if c.JWT.ExpireHour <= 0 {
|
||||||
return fmt.Errorf("jwt.expire_hour must be positive")
|
return fmt.Errorf("jwt.expire_hour must be positive")
|
||||||
}
|
}
|
||||||
@@ -940,20 +1372,20 @@ func (c *Config) Validate() error {
|
|||||||
return fmt.Errorf("jwt.expire_hour must be <= 168 (7 days)")
|
return fmt.Errorf("jwt.expire_hour must be <= 168 (7 days)")
|
||||||
}
|
}
|
||||||
if c.JWT.ExpireHour > 24 {
|
if c.JWT.ExpireHour > 24 {
|
||||||
log.Printf("Warning: jwt.expire_hour is %d hours (> 24). Consider shorter expiration for security.", c.JWT.ExpireHour)
|
slog.Warn("jwt.expire_hour is high; consider shorter expiration for security", "expire_hour", c.JWT.ExpireHour)
|
||||||
}
|
}
|
||||||
// JWT Refresh Token配置验证
|
// JWT Refresh Token配置验证
|
||||||
if c.JWT.AccessTokenExpireMinutes <= 0 {
|
if c.JWT.AccessTokenExpireMinutes < 0 {
|
||||||
return fmt.Errorf("jwt.access_token_expire_minutes must be positive")
|
return fmt.Errorf("jwt.access_token_expire_minutes must be non-negative")
|
||||||
}
|
}
|
||||||
if c.JWT.AccessTokenExpireMinutes > 720 {
|
if c.JWT.AccessTokenExpireMinutes > 720 {
|
||||||
log.Printf("Warning: jwt.access_token_expire_minutes is %d (> 720). Consider shorter expiration for security.", c.JWT.AccessTokenExpireMinutes)
|
slog.Warn("jwt.access_token_expire_minutes is high; consider shorter expiration for security", "access_token_expire_minutes", c.JWT.AccessTokenExpireMinutes)
|
||||||
}
|
}
|
||||||
if c.JWT.RefreshTokenExpireDays <= 0 {
|
if c.JWT.RefreshTokenExpireDays <= 0 {
|
||||||
return fmt.Errorf("jwt.refresh_token_expire_days must be positive")
|
return fmt.Errorf("jwt.refresh_token_expire_days must be positive")
|
||||||
}
|
}
|
||||||
if c.JWT.RefreshTokenExpireDays > 90 {
|
if c.JWT.RefreshTokenExpireDays > 90 {
|
||||||
log.Printf("Warning: jwt.refresh_token_expire_days is %d (> 90). Consider shorter expiration for security.", c.JWT.RefreshTokenExpireDays)
|
slog.Warn("jwt.refresh_token_expire_days is high; consider shorter expiration for security", "refresh_token_expire_days", c.JWT.RefreshTokenExpireDays)
|
||||||
}
|
}
|
||||||
if c.JWT.RefreshWindowMinutes < 0 {
|
if c.JWT.RefreshWindowMinutes < 0 {
|
||||||
return fmt.Errorf("jwt.refresh_window_minutes must be non-negative")
|
return fmt.Errorf("jwt.refresh_window_minutes must be non-negative")
|
||||||
@@ -1159,9 +1591,116 @@ func (c *Config) Validate() error {
|
|||||||
return fmt.Errorf("usage_cleanup.task_timeout_seconds must be non-negative")
|
return fmt.Errorf("usage_cleanup.task_timeout_seconds must be non-negative")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if c.Idempotency.DefaultTTLSeconds <= 0 {
|
||||||
|
return fmt.Errorf("idempotency.default_ttl_seconds must be positive")
|
||||||
|
}
|
||||||
|
if c.Idempotency.SystemOperationTTLSeconds <= 0 {
|
||||||
|
return fmt.Errorf("idempotency.system_operation_ttl_seconds must be positive")
|
||||||
|
}
|
||||||
|
if c.Idempotency.ProcessingTimeoutSeconds <= 0 {
|
||||||
|
return fmt.Errorf("idempotency.processing_timeout_seconds must be positive")
|
||||||
|
}
|
||||||
|
if c.Idempotency.FailedRetryBackoffSeconds <= 0 {
|
||||||
|
return fmt.Errorf("idempotency.failed_retry_backoff_seconds must be positive")
|
||||||
|
}
|
||||||
|
if c.Idempotency.MaxStoredResponseLen <= 0 {
|
||||||
|
return fmt.Errorf("idempotency.max_stored_response_len must be positive")
|
||||||
|
}
|
||||||
|
if c.Idempotency.CleanupIntervalSeconds <= 0 {
|
||||||
|
return fmt.Errorf("idempotency.cleanup_interval_seconds must be positive")
|
||||||
|
}
|
||||||
|
if c.Idempotency.CleanupBatchSize <= 0 {
|
||||||
|
return fmt.Errorf("idempotency.cleanup_batch_size must be positive")
|
||||||
|
}
|
||||||
if c.Gateway.MaxBodySize <= 0 {
|
if c.Gateway.MaxBodySize <= 0 {
|
||||||
return fmt.Errorf("gateway.max_body_size must be positive")
|
return fmt.Errorf("gateway.max_body_size must be positive")
|
||||||
}
|
}
|
||||||
|
if c.Gateway.UpstreamResponseReadMaxBytes <= 0 {
|
||||||
|
return fmt.Errorf("gateway.upstream_response_read_max_bytes must be positive")
|
||||||
|
}
|
||||||
|
if c.Gateway.ProxyProbeResponseReadMaxBytes <= 0 {
|
||||||
|
return fmt.Errorf("gateway.proxy_probe_response_read_max_bytes must be positive")
|
||||||
|
}
|
||||||
|
if c.Gateway.SoraMaxBodySize < 0 {
|
||||||
|
return fmt.Errorf("gateway.sora_max_body_size must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Gateway.SoraStreamTimeoutSeconds < 0 {
|
||||||
|
return fmt.Errorf("gateway.sora_stream_timeout_seconds must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Gateway.SoraRequestTimeoutSeconds < 0 {
|
||||||
|
return fmt.Errorf("gateway.sora_request_timeout_seconds must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Gateway.SoraMediaSignedURLTTLSeconds < 0 {
|
||||||
|
return fmt.Errorf("gateway.sora_media_signed_url_ttl_seconds must be non-negative")
|
||||||
|
}
|
||||||
|
if mode := strings.TrimSpace(strings.ToLower(c.Gateway.SoraStreamMode)); mode != "" {
|
||||||
|
switch mode {
|
||||||
|
case "force", "error":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("gateway.sora_stream_mode must be one of: force/error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Sora.Client.TimeoutSeconds < 0 {
|
||||||
|
return fmt.Errorf("sora.client.timeout_seconds must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Client.MaxRetries < 0 {
|
||||||
|
return fmt.Errorf("sora.client.max_retries must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Client.CloudflareChallengeCooldownSeconds < 0 {
|
||||||
|
return fmt.Errorf("sora.client.cloudflare_challenge_cooldown_seconds must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Client.PollIntervalSeconds < 0 {
|
||||||
|
return fmt.Errorf("sora.client.poll_interval_seconds must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Client.MaxPollAttempts < 0 {
|
||||||
|
return fmt.Errorf("sora.client.max_poll_attempts must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Client.RecentTaskLimit < 0 {
|
||||||
|
return fmt.Errorf("sora.client.recent_task_limit must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Client.RecentTaskLimitMax < 0 {
|
||||||
|
return fmt.Errorf("sora.client.recent_task_limit_max must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Client.RecentTaskLimitMax > 0 && c.Sora.Client.RecentTaskLimit > 0 &&
|
||||||
|
c.Sora.Client.RecentTaskLimitMax < c.Sora.Client.RecentTaskLimit {
|
||||||
|
c.Sora.Client.RecentTaskLimitMax = c.Sora.Client.RecentTaskLimit
|
||||||
|
}
|
||||||
|
if c.Sora.Client.CurlCFFISidecar.TimeoutSeconds < 0 {
|
||||||
|
return fmt.Errorf("sora.client.curl_cffi_sidecar.timeout_seconds must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Client.CurlCFFISidecar.SessionTTLSeconds < 0 {
|
||||||
|
return fmt.Errorf("sora.client.curl_cffi_sidecar.session_ttl_seconds must be non-negative")
|
||||||
|
}
|
||||||
|
if !c.Sora.Client.CurlCFFISidecar.Enabled {
|
||||||
|
return fmt.Errorf("sora.client.curl_cffi_sidecar.enabled must be true")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(c.Sora.Client.CurlCFFISidecar.BaseURL) == "" {
|
||||||
|
return fmt.Errorf("sora.client.curl_cffi_sidecar.base_url is required")
|
||||||
|
}
|
||||||
|
if c.Sora.Storage.MaxConcurrentDownloads < 0 {
|
||||||
|
return fmt.Errorf("sora.storage.max_concurrent_downloads must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Storage.DownloadTimeoutSeconds < 0 {
|
||||||
|
return fmt.Errorf("sora.storage.download_timeout_seconds must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Storage.MaxDownloadBytes < 0 {
|
||||||
|
return fmt.Errorf("sora.storage.max_download_bytes must be non-negative")
|
||||||
|
}
|
||||||
|
if c.Sora.Storage.Cleanup.Enabled {
|
||||||
|
if c.Sora.Storage.Cleanup.RetentionDays <= 0 {
|
||||||
|
return fmt.Errorf("sora.storage.cleanup.retention_days must be positive")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(c.Sora.Storage.Cleanup.Schedule) == "" {
|
||||||
|
return fmt.Errorf("sora.storage.cleanup.schedule is required when cleanup is enabled")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if c.Sora.Storage.Cleanup.RetentionDays < 0 {
|
||||||
|
return fmt.Errorf("sora.storage.cleanup.retention_days must be non-negative")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if storageType := strings.TrimSpace(strings.ToLower(c.Sora.Storage.Type)); storageType != "" && storageType != "local" {
|
||||||
|
return fmt.Errorf("sora.storage.type must be 'local'")
|
||||||
|
}
|
||||||
if strings.TrimSpace(c.Gateway.ConnectionPoolIsolation) != "" {
|
if strings.TrimSpace(c.Gateway.ConnectionPoolIsolation) != "" {
|
||||||
switch c.Gateway.ConnectionPoolIsolation {
|
switch c.Gateway.ConnectionPoolIsolation {
|
||||||
case ConnectionPoolIsolationProxy, ConnectionPoolIsolationAccount, ConnectionPoolIsolationAccountProxy:
|
case ConnectionPoolIsolationProxy, ConnectionPoolIsolationAccount, ConnectionPoolIsolationAccountProxy:
|
||||||
@@ -1183,7 +1722,7 @@ func (c *Config) Validate() error {
|
|||||||
return fmt.Errorf("gateway.idle_conn_timeout_seconds must be positive")
|
return fmt.Errorf("gateway.idle_conn_timeout_seconds must be positive")
|
||||||
}
|
}
|
||||||
if c.Gateway.IdleConnTimeoutSeconds > 180 {
|
if c.Gateway.IdleConnTimeoutSeconds > 180 {
|
||||||
log.Printf("Warning: gateway.idle_conn_timeout_seconds is %d (> 180). Consider 60-120 seconds for better connection reuse.", c.Gateway.IdleConnTimeoutSeconds)
|
slog.Warn("gateway.idle_conn_timeout_seconds is high; consider 60-120 seconds for better connection reuse", "idle_conn_timeout_seconds", c.Gateway.IdleConnTimeoutSeconds)
|
||||||
}
|
}
|
||||||
if c.Gateway.MaxUpstreamClients <= 0 {
|
if c.Gateway.MaxUpstreamClients <= 0 {
|
||||||
return fmt.Errorf("gateway.max_upstream_clients must be positive")
|
return fmt.Errorf("gateway.max_upstream_clients must be positive")
|
||||||
@@ -1214,6 +1753,70 @@ func (c *Config) Validate() error {
|
|||||||
if c.Gateway.MaxLineSize != 0 && c.Gateway.MaxLineSize < 1024*1024 {
|
if c.Gateway.MaxLineSize != 0 && c.Gateway.MaxLineSize < 1024*1024 {
|
||||||
return fmt.Errorf("gateway.max_line_size must be at least 1MB")
|
return fmt.Errorf("gateway.max_line_size must be at least 1MB")
|
||||||
}
|
}
|
||||||
|
if c.Gateway.UsageRecord.WorkerCount <= 0 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.worker_count must be positive")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.QueueSize <= 0 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.queue_size must be positive")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.TaskTimeoutSeconds <= 0 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.task_timeout_seconds must be positive")
|
||||||
|
}
|
||||||
|
switch strings.ToLower(strings.TrimSpace(c.Gateway.UsageRecord.OverflowPolicy)) {
|
||||||
|
case UsageRecordOverflowPolicyDrop, UsageRecordOverflowPolicySample, UsageRecordOverflowPolicySync:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("gateway.usage_record.overflow_policy must be one of: %s/%s/%s",
|
||||||
|
UsageRecordOverflowPolicyDrop, UsageRecordOverflowPolicySample, UsageRecordOverflowPolicySync)
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.OverflowSamplePercent < 0 || c.Gateway.UsageRecord.OverflowSamplePercent > 100 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.overflow_sample_percent must be between 0-100")
|
||||||
|
}
|
||||||
|
if strings.EqualFold(strings.TrimSpace(c.Gateway.UsageRecord.OverflowPolicy), UsageRecordOverflowPolicySample) &&
|
||||||
|
c.Gateway.UsageRecord.OverflowSamplePercent <= 0 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.overflow_sample_percent must be positive when overflow_policy=sample")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleEnabled {
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleMinWorkers <= 0 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.auto_scale_min_workers must be positive")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleMaxWorkers <= 0 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.auto_scale_max_workers must be positive")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleMaxWorkers < c.Gateway.UsageRecord.AutoScaleMinWorkers {
|
||||||
|
return fmt.Errorf("gateway.usage_record.auto_scale_max_workers must be >= auto_scale_min_workers")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.WorkerCount < c.Gateway.UsageRecord.AutoScaleMinWorkers ||
|
||||||
|
c.Gateway.UsageRecord.WorkerCount > c.Gateway.UsageRecord.AutoScaleMaxWorkers {
|
||||||
|
return fmt.Errorf("gateway.usage_record.worker_count must be between auto_scale_min_workers and auto_scale_max_workers")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleUpQueuePercent <= 0 || c.Gateway.UsageRecord.AutoScaleUpQueuePercent > 100 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.auto_scale_up_queue_percent must be between 1-100")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleDownQueuePercent < 0 || c.Gateway.UsageRecord.AutoScaleDownQueuePercent >= 100 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.auto_scale_down_queue_percent must be between 0-99")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleDownQueuePercent >= c.Gateway.UsageRecord.AutoScaleUpQueuePercent {
|
||||||
|
return fmt.Errorf("gateway.usage_record.auto_scale_down_queue_percent must be less than auto_scale_up_queue_percent")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleUpStep <= 0 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.auto_scale_up_step must be positive")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleDownStep <= 0 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.auto_scale_down_step must be positive")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds <= 0 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.auto_scale_check_interval_seconds must be positive")
|
||||||
|
}
|
||||||
|
if c.Gateway.UsageRecord.AutoScaleCooldownSeconds < 0 {
|
||||||
|
return fmt.Errorf("gateway.usage_record.auto_scale_cooldown_seconds must be non-negative")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Gateway.UserGroupRateCacheTTLSeconds <= 0 {
|
||||||
|
return fmt.Errorf("gateway.user_group_rate_cache_ttl_seconds must be positive")
|
||||||
|
}
|
||||||
|
if c.Gateway.ModelsListCacheTTLSeconds < 10 || c.Gateway.ModelsListCacheTTLSeconds > 30 {
|
||||||
|
return fmt.Errorf("gateway.models_list_cache_ttl_seconds must be between 10-30")
|
||||||
|
}
|
||||||
if c.Gateway.Scheduling.StickySessionMaxWaiting <= 0 {
|
if c.Gateway.Scheduling.StickySessionMaxWaiting <= 0 {
|
||||||
return fmt.Errorf("gateway.scheduling.sticky_session_max_waiting must be positive")
|
return fmt.Errorf("gateway.scheduling.sticky_session_max_waiting must be positive")
|
||||||
}
|
}
|
||||||
@@ -1420,6 +2023,6 @@ func warnIfInsecureURL(field, raw string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.EqualFold(u.Scheme, "http") {
|
if strings.EqualFold(u.Scheme, "http") {
|
||||||
log.Printf("Warning: %s uses http scheme; use https in production to avoid token leakage.", field)
|
slog.Warn("url uses http scheme; use https in production to avoid token leakage", "field", field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,25 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func resetViperWithJWTSecret(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
viper.Reset()
|
||||||
|
t.Setenv("JWT_SECRET", strings.Repeat("x", 32))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadForBootstrapAllowsMissingJWTSecret(t *testing.T) {
|
||||||
|
viper.Reset()
|
||||||
|
t.Setenv("JWT_SECRET", "")
|
||||||
|
|
||||||
|
cfg, err := LoadForBootstrap()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadForBootstrap() error: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.JWT.Secret != "" {
|
||||||
|
t.Fatalf("LoadForBootstrap() should keep empty jwt.secret during bootstrap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeRunMode(t *testing.T) {
|
func TestNormalizeRunMode(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
@@ -29,7 +48,7 @@ func TestNormalizeRunMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDefaultSchedulingConfig(t *testing.T) {
|
func TestLoadDefaultSchedulingConfig(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -56,8 +75,44 @@ func TestLoadDefaultSchedulingConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadDefaultIdempotencyConfig(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.Idempotency.ObserveOnly {
|
||||||
|
t.Fatalf("Idempotency.ObserveOnly = false, want true")
|
||||||
|
}
|
||||||
|
if cfg.Idempotency.DefaultTTLSeconds != 86400 {
|
||||||
|
t.Fatalf("Idempotency.DefaultTTLSeconds = %d, want 86400", cfg.Idempotency.DefaultTTLSeconds)
|
||||||
|
}
|
||||||
|
if cfg.Idempotency.SystemOperationTTLSeconds != 3600 {
|
||||||
|
t.Fatalf("Idempotency.SystemOperationTTLSeconds = %d, want 3600", cfg.Idempotency.SystemOperationTTLSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadIdempotencyConfigFromEnv(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
t.Setenv("IDEMPOTENCY_OBSERVE_ONLY", "false")
|
||||||
|
t.Setenv("IDEMPOTENCY_DEFAULT_TTL_SECONDS", "600")
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Idempotency.ObserveOnly {
|
||||||
|
t.Fatalf("Idempotency.ObserveOnly = true, want false")
|
||||||
|
}
|
||||||
|
if cfg.Idempotency.DefaultTTLSeconds != 600 {
|
||||||
|
t.Fatalf("Idempotency.DefaultTTLSeconds = %d, want 600", cfg.Idempotency.DefaultTTLSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadSchedulingConfigFromEnv(t *testing.T) {
|
func TestLoadSchedulingConfigFromEnv(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
t.Setenv("GATEWAY_SCHEDULING_STICKY_SESSION_MAX_WAITING", "5")
|
t.Setenv("GATEWAY_SCHEDULING_STICKY_SESSION_MAX_WAITING", "5")
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
@@ -71,7 +126,7 @@ func TestLoadSchedulingConfigFromEnv(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDefaultSecurityToggles(t *testing.T) {
|
func TestLoadDefaultSecurityToggles(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -87,13 +142,69 @@ func TestLoadDefaultSecurityToggles(t *testing.T) {
|
|||||||
if !cfg.Security.URLAllowlist.AllowPrivateHosts {
|
if !cfg.Security.URLAllowlist.AllowPrivateHosts {
|
||||||
t.Fatalf("URLAllowlist.AllowPrivateHosts = false, want true")
|
t.Fatalf("URLAllowlist.AllowPrivateHosts = false, want true")
|
||||||
}
|
}
|
||||||
if cfg.Security.ResponseHeaders.Enabled {
|
if !cfg.Security.ResponseHeaders.Enabled {
|
||||||
t.Fatalf("ResponseHeaders.Enabled = true, want false")
|
t.Fatalf("ResponseHeaders.Enabled = false, want true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDefaultServerMode(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Server.Mode != "release" {
|
||||||
|
t.Fatalf("Server.Mode = %q, want %q", cfg.Server.Mode, "release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDefaultJWTAccessTokenExpireMinutes(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.JWT.ExpireHour != 24 {
|
||||||
|
t.Fatalf("JWT.ExpireHour = %d, want 24", cfg.JWT.ExpireHour)
|
||||||
|
}
|
||||||
|
if cfg.JWT.AccessTokenExpireMinutes != 0 {
|
||||||
|
t.Fatalf("JWT.AccessTokenExpireMinutes = %d, want 0", cfg.JWT.AccessTokenExpireMinutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadJWTAccessTokenExpireMinutesFromEnv(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
t.Setenv("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", "90")
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.JWT.AccessTokenExpireMinutes != 90 {
|
||||||
|
t.Fatalf("JWT.AccessTokenExpireMinutes = %d, want 90", cfg.JWT.AccessTokenExpireMinutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadDefaultDatabaseSSLMode(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Database.SSLMode != "prefer" {
|
||||||
|
t.Fatalf("Database.SSLMode = %q, want %q", cfg.Database.SSLMode, "prefer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateLinuxDoFrontendRedirectURL(t *testing.T) {
|
func TestValidateLinuxDoFrontendRedirectURL(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -118,7 +229,7 @@ func TestValidateLinuxDoFrontendRedirectURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateLinuxDoPKCERequiredForPublicClient(t *testing.T) {
|
func TestValidateLinuxDoPKCERequiredForPublicClient(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -143,7 +254,7 @@ func TestValidateLinuxDoPKCERequiredForPublicClient(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDefaultDashboardCacheConfig(t *testing.T) {
|
func TestLoadDefaultDashboardCacheConfig(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -168,7 +279,7 @@ func TestLoadDefaultDashboardCacheConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateDashboardCacheConfigEnabled(t *testing.T) {
|
func TestValidateDashboardCacheConfigEnabled(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -188,7 +299,7 @@ func TestValidateDashboardCacheConfigEnabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateDashboardCacheConfigDisabled(t *testing.T) {
|
func TestValidateDashboardCacheConfigDisabled(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -207,7 +318,7 @@ func TestValidateDashboardCacheConfigDisabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDefaultDashboardAggregationConfig(t *testing.T) {
|
func TestLoadDefaultDashboardAggregationConfig(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -244,7 +355,7 @@ func TestLoadDefaultDashboardAggregationConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateDashboardAggregationConfigDisabled(t *testing.T) {
|
func TestValidateDashboardAggregationConfigDisabled(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -263,7 +374,7 @@ func TestValidateDashboardAggregationConfigDisabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateDashboardAggregationBackfillMaxDays(t *testing.T) {
|
func TestValidateDashboardAggregationBackfillMaxDays(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -282,7 +393,7 @@ func TestValidateDashboardAggregationBackfillMaxDays(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDefaultUsageCleanupConfig(t *testing.T) {
|
func TestLoadDefaultUsageCleanupConfig(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -307,7 +418,7 @@ func TestLoadDefaultUsageCleanupConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateUsageCleanupConfigEnabled(t *testing.T) {
|
func TestValidateUsageCleanupConfigEnabled(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -326,7 +437,7 @@ func TestValidateUsageCleanupConfigEnabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateUsageCleanupConfigDisabled(t *testing.T) {
|
func TestValidateUsageCleanupConfigDisabled(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -424,6 +535,40 @@ func TestValidateAbsoluteHTTPURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateServerFrontendURL(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Server.FrontendURL = "https://example.com"
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
t.Fatalf("Validate() frontend_url valid error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Server.FrontendURL = "https://example.com/path"
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
t.Fatalf("Validate() frontend_url with path valid error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Server.FrontendURL = "https://example.com?utm=1"
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Fatalf("Validate() should reject server.frontend_url with query")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Server.FrontendURL = "https://user:pass@example.com"
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Fatalf("Validate() should reject server.frontend_url with userinfo")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Server.FrontendURL = "/relative"
|
||||||
|
if err := cfg.Validate(); err == nil {
|
||||||
|
t.Fatalf("Validate() should reject relative server.frontend_url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateFrontendRedirectURL(t *testing.T) {
|
func TestValidateFrontendRedirectURL(t *testing.T) {
|
||||||
if err := ValidateFrontendRedirectURL("/auth/callback"); err != nil {
|
if err := ValidateFrontendRedirectURL("/auth/callback"); err != nil {
|
||||||
t.Fatalf("ValidateFrontendRedirectURL relative error: %v", err)
|
t.Fatalf("ValidateFrontendRedirectURL relative error: %v", err)
|
||||||
@@ -445,6 +590,7 @@ func TestValidateFrontendRedirectURL(t *testing.T) {
|
|||||||
func TestWarnIfInsecureURL(t *testing.T) {
|
func TestWarnIfInsecureURL(t *testing.T) {
|
||||||
warnIfInsecureURL("test", "http://example.com")
|
warnIfInsecureURL("test", "http://example.com")
|
||||||
warnIfInsecureURL("test", "bad://url")
|
warnIfInsecureURL("test", "bad://url")
|
||||||
|
warnIfInsecureURL("test", "://invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateJWTSecretDefaultLength(t *testing.T) {
|
func TestGenerateJWTSecretDefaultLength(t *testing.T) {
|
||||||
@@ -458,7 +604,7 @@ func TestGenerateJWTSecretDefaultLength(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateOpsCleanupScheduleRequired(t *testing.T) {
|
func TestValidateOpsCleanupScheduleRequired(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -476,7 +622,7 @@ func TestValidateOpsCleanupScheduleRequired(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateConcurrencyPingInterval(t *testing.T) {
|
func TestValidateConcurrencyPingInterval(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -493,14 +639,14 @@ func TestValidateConcurrencyPingInterval(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestProvideConfig(t *testing.T) {
|
func TestProvideConfig(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
if _, err := ProvideConfig(); err != nil {
|
if _, err := ProvideConfig(); err != nil {
|
||||||
t.Fatalf("ProvideConfig() error: %v", err)
|
t.Fatalf("ProvideConfig() error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateConfigWithLinuxDoEnabled(t *testing.T) {
|
func TestValidateConfigWithLinuxDoEnabled(t *testing.T) {
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -544,6 +690,24 @@ func TestGenerateJWTSecretWithLength(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDatabaseDSNWithTimezone_WithPassword(t *testing.T) {
|
||||||
|
d := &DatabaseConfig{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
User: "u",
|
||||||
|
Password: "p",
|
||||||
|
DBName: "db",
|
||||||
|
SSLMode: "prefer",
|
||||||
|
}
|
||||||
|
got := d.DSNWithTimezone("UTC")
|
||||||
|
if !strings.Contains(got, "password=p") {
|
||||||
|
t.Fatalf("DSNWithTimezone should include password: %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "TimeZone=UTC") {
|
||||||
|
t.Fatalf("DSNWithTimezone should include TimeZone=UTC: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateAbsoluteHTTPURLMissingHost(t *testing.T) {
|
func TestValidateAbsoluteHTTPURLMissingHost(t *testing.T) {
|
||||||
if err := ValidateAbsoluteHTTPURL("https://"); err == nil {
|
if err := ValidateAbsoluteHTTPURL("https://"); err == nil {
|
||||||
t.Fatalf("ValidateAbsoluteHTTPURL should reject missing host")
|
t.Fatalf("ValidateAbsoluteHTTPURL should reject missing host")
|
||||||
@@ -566,10 +730,35 @@ func TestWarnIfInsecureURLHTTPS(t *testing.T) {
|
|||||||
warnIfInsecureURL("secure", "https://example.com")
|
warnIfInsecureURL("secure", "https://example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateJWTSecret_UTF8Bytes(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 31 bytes (< 32) even though it's 31 characters.
|
||||||
|
cfg.JWT.Secret = strings.Repeat("a", 31)
|
||||||
|
err = cfg.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Validate() should reject 31-byte secret")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "at least 32 bytes") {
|
||||||
|
t.Fatalf("Validate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 32 bytes OK.
|
||||||
|
cfg.JWT.Secret = strings.Repeat("a", 32)
|
||||||
|
err = cfg.Validate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Validate() should accept 32-byte secret: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestValidateConfigErrors(t *testing.T) {
|
func TestValidateConfigErrors(t *testing.T) {
|
||||||
buildValid := func(t *testing.T) *Config {
|
buildValid := func(t *testing.T) *Config {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
viper.Reset()
|
resetViperWithJWTSecret(t)
|
||||||
cfg, err := Load()
|
cfg, err := Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Load() error: %v", err)
|
t.Fatalf("Load() error: %v", err)
|
||||||
@@ -582,6 +771,26 @@ func TestValidateConfigErrors(t *testing.T) {
|
|||||||
mutate func(*Config)
|
mutate func(*Config)
|
||||||
wantErr string
|
wantErr string
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
name: "jwt secret required",
|
||||||
|
mutate: func(c *Config) { c.JWT.Secret = "" },
|
||||||
|
wantErr: "jwt.secret is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jwt secret min bytes",
|
||||||
|
mutate: func(c *Config) { c.JWT.Secret = strings.Repeat("a", 31) },
|
||||||
|
wantErr: "jwt.secret must be at least 32 bytes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subscription maintenance worker_count non-negative",
|
||||||
|
mutate: func(c *Config) { c.SubscriptionMaintenance.WorkerCount = -1 },
|
||||||
|
wantErr: "subscription_maintenance.worker_count",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subscription maintenance queue_size non-negative",
|
||||||
|
mutate: func(c *Config) { c.SubscriptionMaintenance.QueueSize = -1 },
|
||||||
|
wantErr: "subscription_maintenance.queue_size",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "jwt expire hour positive",
|
name: "jwt expire hour positive",
|
||||||
mutate: func(c *Config) { c.JWT.ExpireHour = 0 },
|
mutate: func(c *Config) { c.JWT.ExpireHour = 0 },
|
||||||
@@ -592,6 +801,11 @@ func TestValidateConfigErrors(t *testing.T) {
|
|||||||
mutate: func(c *Config) { c.JWT.ExpireHour = 200 },
|
mutate: func(c *Config) { c.JWT.ExpireHour = 200 },
|
||||||
wantErr: "jwt.expire_hour must be <= 168",
|
wantErr: "jwt.expire_hour must be <= 168",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "jwt access token expire minutes non-negative",
|
||||||
|
mutate: func(c *Config) { c.JWT.AccessTokenExpireMinutes = -1 },
|
||||||
|
wantErr: "jwt.access_token_expire_minutes must be non-negative",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "csp policy required",
|
name: "csp policy required",
|
||||||
mutate: func(c *Config) { c.Security.CSP.Enabled = true; c.Security.CSP.Policy = "" },
|
mutate: func(c *Config) { c.Security.CSP.Enabled = true; c.Security.CSP.Policy = "" },
|
||||||
@@ -799,6 +1013,84 @@ func TestValidateConfigErrors(t *testing.T) {
|
|||||||
mutate: func(c *Config) { c.Gateway.MaxLineSize = -1 },
|
mutate: func(c *Config) { c.Gateway.MaxLineSize = -1 },
|
||||||
wantErr: "gateway.max_line_size must be non-negative",
|
wantErr: "gateway.max_line_size must be non-negative",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record worker count",
|
||||||
|
mutate: func(c *Config) { c.Gateway.UsageRecord.WorkerCount = 0 },
|
||||||
|
wantErr: "gateway.usage_record.worker_count",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record queue size",
|
||||||
|
mutate: func(c *Config) { c.Gateway.UsageRecord.QueueSize = 0 },
|
||||||
|
wantErr: "gateway.usage_record.queue_size",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record timeout",
|
||||||
|
mutate: func(c *Config) { c.Gateway.UsageRecord.TaskTimeoutSeconds = 0 },
|
||||||
|
wantErr: "gateway.usage_record.task_timeout_seconds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record overflow policy",
|
||||||
|
mutate: func(c *Config) { c.Gateway.UsageRecord.OverflowPolicy = "invalid" },
|
||||||
|
wantErr: "gateway.usage_record.overflow_policy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record sample percent range",
|
||||||
|
mutate: func(c *Config) { c.Gateway.UsageRecord.OverflowSamplePercent = 101 },
|
||||||
|
wantErr: "gateway.usage_record.overflow_sample_percent",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record sample percent required for sample policy",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Gateway.UsageRecord.OverflowPolicy = UsageRecordOverflowPolicySample
|
||||||
|
c.Gateway.UsageRecord.OverflowSamplePercent = 0
|
||||||
|
},
|
||||||
|
wantErr: "gateway.usage_record.overflow_sample_percent must be positive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record auto scale max gte min",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Gateway.UsageRecord.AutoScaleMinWorkers = 256
|
||||||
|
c.Gateway.UsageRecord.AutoScaleMaxWorkers = 128
|
||||||
|
},
|
||||||
|
wantErr: "gateway.usage_record.auto_scale_max_workers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record worker in auto scale range",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Gateway.UsageRecord.AutoScaleMinWorkers = 200
|
||||||
|
c.Gateway.UsageRecord.AutoScaleMaxWorkers = 300
|
||||||
|
c.Gateway.UsageRecord.WorkerCount = 128
|
||||||
|
},
|
||||||
|
wantErr: "gateway.usage_record.worker_count must be between auto_scale_min_workers and auto_scale_max_workers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record auto scale queue thresholds order",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Gateway.UsageRecord.AutoScaleUpQueuePercent = 50
|
||||||
|
c.Gateway.UsageRecord.AutoScaleDownQueuePercent = 50
|
||||||
|
},
|
||||||
|
wantErr: "gateway.usage_record.auto_scale_down_queue_percent must be less",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record auto scale up step",
|
||||||
|
mutate: func(c *Config) { c.Gateway.UsageRecord.AutoScaleUpStep = 0 },
|
||||||
|
wantErr: "gateway.usage_record.auto_scale_up_step",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway usage record auto scale interval",
|
||||||
|
mutate: func(c *Config) { c.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds = 0 },
|
||||||
|
wantErr: "gateway.usage_record.auto_scale_check_interval_seconds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway user group rate cache ttl",
|
||||||
|
mutate: func(c *Config) { c.Gateway.UserGroupRateCacheTTLSeconds = 0 },
|
||||||
|
wantErr: "gateway.user_group_rate_cache_ttl_seconds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gateway models list cache ttl range",
|
||||||
|
mutate: func(c *Config) { c.Gateway.ModelsListCacheTTLSeconds = 31 },
|
||||||
|
wantErr: "gateway.models_list_cache_ttl_seconds",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "gateway scheduling sticky waiting",
|
name: "gateway scheduling sticky waiting",
|
||||||
mutate: func(c *Config) { c.Gateway.Scheduling.StickySessionMaxWaiting = 0 },
|
mutate: func(c *Config) { c.Gateway.Scheduling.StickySessionMaxWaiting = 0 },
|
||||||
@@ -822,6 +1114,37 @@ func TestValidateConfigErrors(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: "gateway.scheduling.outbox_lag_rebuild_seconds",
|
wantErr: "gateway.scheduling.outbox_lag_rebuild_seconds",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "log level invalid",
|
||||||
|
mutate: func(c *Config) { c.Log.Level = "trace" },
|
||||||
|
wantErr: "log.level",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "log format invalid",
|
||||||
|
mutate: func(c *Config) { c.Log.Format = "plain" },
|
||||||
|
wantErr: "log.format",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "log output disabled",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Log.Output.ToStdout = false
|
||||||
|
c.Log.Output.ToFile = false
|
||||||
|
},
|
||||||
|
wantErr: "log.output.to_stdout and log.output.to_file cannot both be false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "log rotation size",
|
||||||
|
mutate: func(c *Config) { c.Log.Rotation.MaxSizeMB = 0 },
|
||||||
|
wantErr: "log.rotation.max_size_mb",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "log sampling enabled invalid",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Log.Sampling.Enabled = true
|
||||||
|
c.Log.Sampling.Initial = 0
|
||||||
|
},
|
||||||
|
wantErr: "log.sampling.initial",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "ops metrics collector ttl",
|
name: "ops metrics collector ttl",
|
||||||
mutate: func(c *Config) { c.Ops.MetricsCollectorCache.TTL = -1 },
|
mutate: func(c *Config) { c.Ops.MetricsCollectorCache.TTL = -1 },
|
||||||
@@ -850,3 +1173,234 @@ func TestValidateConfigErrors(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateConfig_AutoScaleDisabledIgnoreAutoScaleFields(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Gateway.UsageRecord.AutoScaleEnabled = false
|
||||||
|
cfg.Gateway.UsageRecord.WorkerCount = 64
|
||||||
|
|
||||||
|
// 自动扩缩容关闭时,这些字段应被忽略,不应导致校验失败。
|
||||||
|
cfg.Gateway.UsageRecord.AutoScaleMinWorkers = 0
|
||||||
|
cfg.Gateway.UsageRecord.AutoScaleMaxWorkers = 0
|
||||||
|
cfg.Gateway.UsageRecord.AutoScaleUpQueuePercent = 0
|
||||||
|
cfg.Gateway.UsageRecord.AutoScaleDownQueuePercent = 100
|
||||||
|
cfg.Gateway.UsageRecord.AutoScaleUpStep = 0
|
||||||
|
cfg.Gateway.UsageRecord.AutoScaleDownStep = 0
|
||||||
|
cfg.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds = 0
|
||||||
|
cfg.Gateway.UsageRecord.AutoScaleCooldownSeconds = -1
|
||||||
|
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
t.Fatalf("Validate() should ignore auto scale fields when disabled: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateConfig_LogRequiredAndRotationBounds(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
mutate func(*Config)
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "log level required",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Log.Level = ""
|
||||||
|
},
|
||||||
|
wantErr: "log.level is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "log format required",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Log.Format = ""
|
||||||
|
},
|
||||||
|
wantErr: "log.format is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "log stacktrace required",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Log.StacktraceLevel = ""
|
||||||
|
},
|
||||||
|
wantErr: "log.stacktrace_level is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "log max backups non-negative",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Log.Rotation.MaxBackups = -1
|
||||||
|
},
|
||||||
|
wantErr: "log.rotation.max_backups must be non-negative",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "log max age non-negative",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Log.Rotation.MaxAgeDays = -1
|
||||||
|
},
|
||||||
|
wantErr: "log.rotation.max_age_days must be non-negative",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sampling thereafter non-negative when disabled",
|
||||||
|
mutate: func(c *Config) {
|
||||||
|
c.Log.Sampling.Enabled = false
|
||||||
|
c.Log.Sampling.Thereafter = -1
|
||||||
|
},
|
||||||
|
wantErr: "log.sampling.thereafter must be non-negative",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range cases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
tt.mutate(cfg)
|
||||||
|
err = cfg.Validate()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("Validate() error = %v, want %q", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSoraCurlCFFISidecarDefaults(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.Sora.Client.CurlCFFISidecar.Enabled {
|
||||||
|
t.Fatalf("Sora curl_cffi sidecar should be enabled by default")
|
||||||
|
}
|
||||||
|
if cfg.Sora.Client.CloudflareChallengeCooldownSeconds <= 0 {
|
||||||
|
t.Fatalf("Sora cloudflare challenge cooldown should be positive by default")
|
||||||
|
}
|
||||||
|
if cfg.Sora.Client.CurlCFFISidecar.BaseURL == "" {
|
||||||
|
t.Fatalf("Sora curl_cffi sidecar base_url should not be empty by default")
|
||||||
|
}
|
||||||
|
if cfg.Sora.Client.CurlCFFISidecar.Impersonate == "" {
|
||||||
|
t.Fatalf("Sora curl_cffi sidecar impersonate should not be empty by default")
|
||||||
|
}
|
||||||
|
if !cfg.Sora.Client.CurlCFFISidecar.SessionReuseEnabled {
|
||||||
|
t.Fatalf("Sora curl_cffi sidecar session reuse should be enabled by default")
|
||||||
|
}
|
||||||
|
if cfg.Sora.Client.CurlCFFISidecar.SessionTTLSeconds <= 0 {
|
||||||
|
t.Fatalf("Sora curl_cffi sidecar session ttl should be positive by default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSoraCurlCFFISidecarRequired(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Sora.Client.CurlCFFISidecar.Enabled = false
|
||||||
|
err = cfg.Validate()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "sora.client.curl_cffi_sidecar.enabled must be true") {
|
||||||
|
t.Fatalf("Validate() error = %v, want sidecar enabled error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSoraCurlCFFISidecarBaseURLRequired(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Sora.Client.CurlCFFISidecar.BaseURL = " "
|
||||||
|
err = cfg.Validate()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "sora.client.curl_cffi_sidecar.base_url is required") {
|
||||||
|
t.Fatalf("Validate() error = %v, want sidecar base_url required error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSoraCurlCFFISidecarSessionTTLNonNegative(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Sora.Client.CurlCFFISidecar.SessionTTLSeconds = -1
|
||||||
|
err = cfg.Validate()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "sora.client.curl_cffi_sidecar.session_ttl_seconds must be non-negative") {
|
||||||
|
t.Fatalf("Validate() error = %v, want sidecar session ttl error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSoraCloudflareChallengeCooldownNonNegative(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Sora.Client.CloudflareChallengeCooldownSeconds = -1
|
||||||
|
err = cfg.Validate()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "sora.client.cloudflare_challenge_cooldown_seconds must be non-negative") {
|
||||||
|
t.Fatalf("Validate() error = %v, want cloudflare cooldown error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad_DefaultGatewayUsageRecordConfig(t *testing.T) {
|
||||||
|
resetViperWithJWTSecret(t)
|
||||||
|
cfg, err := Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.WorkerCount != 128 {
|
||||||
|
t.Fatalf("worker_count = %d, want 128", cfg.Gateway.UsageRecord.WorkerCount)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.QueueSize != 16384 {
|
||||||
|
t.Fatalf("queue_size = %d, want 16384", cfg.Gateway.UsageRecord.QueueSize)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.TaskTimeoutSeconds != 5 {
|
||||||
|
t.Fatalf("task_timeout_seconds = %d, want 5", cfg.Gateway.UsageRecord.TaskTimeoutSeconds)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.OverflowPolicy != UsageRecordOverflowPolicySample {
|
||||||
|
t.Fatalf("overflow_policy = %s, want %s", cfg.Gateway.UsageRecord.OverflowPolicy, UsageRecordOverflowPolicySample)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.OverflowSamplePercent != 10 {
|
||||||
|
t.Fatalf("overflow_sample_percent = %d, want 10", cfg.Gateway.UsageRecord.OverflowSamplePercent)
|
||||||
|
}
|
||||||
|
if !cfg.Gateway.UsageRecord.AutoScaleEnabled {
|
||||||
|
t.Fatalf("auto_scale_enabled = false, want true")
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.AutoScaleMinWorkers != 128 {
|
||||||
|
t.Fatalf("auto_scale_min_workers = %d, want 128", cfg.Gateway.UsageRecord.AutoScaleMinWorkers)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.AutoScaleMaxWorkers != 512 {
|
||||||
|
t.Fatalf("auto_scale_max_workers = %d, want 512", cfg.Gateway.UsageRecord.AutoScaleMaxWorkers)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.AutoScaleUpQueuePercent != 70 {
|
||||||
|
t.Fatalf("auto_scale_up_queue_percent = %d, want 70", cfg.Gateway.UsageRecord.AutoScaleUpQueuePercent)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.AutoScaleDownQueuePercent != 15 {
|
||||||
|
t.Fatalf("auto_scale_down_queue_percent = %d, want 15", cfg.Gateway.UsageRecord.AutoScaleDownQueuePercent)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.AutoScaleUpStep != 32 {
|
||||||
|
t.Fatalf("auto_scale_up_step = %d, want 32", cfg.Gateway.UsageRecord.AutoScaleUpStep)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.AutoScaleDownStep != 16 {
|
||||||
|
t.Fatalf("auto_scale_down_step = %d, want 16", cfg.Gateway.UsageRecord.AutoScaleDownStep)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds != 3 {
|
||||||
|
t.Fatalf("auto_scale_check_interval_seconds = %d, want 3", cfg.Gateway.UsageRecord.AutoScaleCheckIntervalSeconds)
|
||||||
|
}
|
||||||
|
if cfg.Gateway.UsageRecord.AutoScaleCooldownSeconds != 10 {
|
||||||
|
t.Fatalf("auto_scale_cooldown_seconds = %d, want 10", cfg.Gateway.UsageRecord.AutoScaleCooldownSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ var ProviderSet = wire.NewSet(
|
|||||||
|
|
||||||
// ProvideConfig 提供应用配置
|
// ProvideConfig 提供应用配置
|
||||||
func ProvideConfig() (*Config, error) {
|
func ProvideConfig() (*Config, error) {
|
||||||
return Load()
|
return LoadForBootstrap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const (
|
|||||||
PlatformOpenAI = "openai"
|
PlatformOpenAI = "openai"
|
||||||
PlatformGemini = "gemini"
|
PlatformGemini = "gemini"
|
||||||
PlatformAntigravity = "antigravity"
|
PlatformAntigravity = "antigravity"
|
||||||
|
PlatformSora = "sora"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Account type constants
|
// Account type constants
|
||||||
@@ -73,6 +74,7 @@ var DefaultAntigravityModelMapping = map[string]string{
|
|||||||
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking", // 官方模型
|
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking", // 官方模型
|
||||||
"claude-opus-4-6": "claude-opus-4-6-thinking", // 简称映射
|
"claude-opus-4-6": "claude-opus-4-6-thinking", // 简称映射
|
||||||
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking", // 迁移旧模型
|
"claude-opus-4-5-thinking": "claude-opus-4-6-thinking", // 迁移旧模型
|
||||||
|
"claude-sonnet-4-6": "claude-sonnet-4-6",
|
||||||
"claude-sonnet-4-5": "claude-sonnet-4-5",
|
"claude-sonnet-4-5": "claude-sonnet-4-5",
|
||||||
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking",
|
||||||
// Claude 详细版本 ID 映射
|
// Claude 详细版本 ID 映射
|
||||||
@@ -87,14 +89,21 @@ var DefaultAntigravityModelMapping = map[string]string{
|
|||||||
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
|
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
|
||||||
"gemini-2.5-pro": "gemini-2.5-pro",
|
"gemini-2.5-pro": "gemini-2.5-pro",
|
||||||
// Gemini 3 白名单
|
// Gemini 3 白名单
|
||||||
"gemini-3-flash": "gemini-3-flash",
|
"gemini-3-flash": "gemini-3-flash",
|
||||||
"gemini-3-pro-high": "gemini-3-pro-high",
|
"gemini-3-pro-high": "gemini-3-pro-high",
|
||||||
"gemini-3-pro-low": "gemini-3-pro-low",
|
"gemini-3-pro-low": "gemini-3-pro-low",
|
||||||
"gemini-3-pro-image": "gemini-3-pro-image",
|
|
||||||
// Gemini 3 preview 映射
|
// Gemini 3 preview 映射
|
||||||
"gemini-3-flash-preview": "gemini-3-flash",
|
"gemini-3-flash-preview": "gemini-3-flash",
|
||||||
"gemini-3-pro-preview": "gemini-3-pro-high",
|
"gemini-3-pro-preview": "gemini-3-pro-high",
|
||||||
"gemini-3-pro-image-preview": "gemini-3-pro-image",
|
// Gemini 3.1 白名单
|
||||||
|
"gemini-3.1-pro-high": "gemini-3.1-pro-high",
|
||||||
|
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
|
||||||
|
// Gemini 3.1 preview 映射
|
||||||
|
"gemini-3.1-pro-preview": "gemini-3.1-pro-high",
|
||||||
|
// Gemini 3.1 image 白名单
|
||||||
|
"gemini-3.1-flash-image": "gemini-3.1-flash-image",
|
||||||
|
// Gemini 3.1 image preview 映射
|
||||||
|
"gemini-3.1-flash-image-preview": "gemini-3.1-flash-image",
|
||||||
// 其他官方模型
|
// 其他官方模型
|
||||||
"gpt-oss-120b-medium": "gpt-oss-120b-medium",
|
"gpt-oss-120b-medium": "gpt-oss-120b-medium",
|
||||||
"tab_flash_lite_preview": "tab_flash_lite_preview",
|
"tab_flash_lite_preview": "tab_flash_lite_preview",
|
||||||
|
|||||||
@@ -175,22 +175,28 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dataPayload := req.Data
|
if err := validateDataHeader(req.Data); err != nil {
|
||||||
if err := validateDataHeader(dataPayload); err != nil {
|
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executeAdminIdempotentJSON(c, "admin.accounts.import_data", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
|
return h.importData(ctx, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest) (DataImportResult, error) {
|
||||||
skipDefaultGroupBind := true
|
skipDefaultGroupBind := true
|
||||||
if req.SkipDefaultGroupBind != nil {
|
if req.SkipDefaultGroupBind != nil {
|
||||||
skipDefaultGroupBind = *req.SkipDefaultGroupBind
|
skipDefaultGroupBind = *req.SkipDefaultGroupBind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataPayload := req.Data
|
||||||
result := DataImportResult{}
|
result := DataImportResult{}
|
||||||
existingProxies, err := h.listAllProxies(c.Request.Context())
|
|
||||||
|
existingProxies, err := h.listAllProxies(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.ErrorFrom(c, err)
|
return result, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyKeyToID := make(map[string]int64, len(existingProxies))
|
proxyKeyToID := make(map[string]int64, len(existingProxies))
|
||||||
@@ -221,8 +227,8 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
|
|||||||
proxyKeyToID[key] = existingID
|
proxyKeyToID[key] = existingID
|
||||||
result.ProxyReused++
|
result.ProxyReused++
|
||||||
if normalizedStatus != "" {
|
if normalizedStatus != "" {
|
||||||
if proxy, err := h.adminService.GetProxy(c.Request.Context(), existingID); err == nil && proxy != nil && proxy.Status != normalizedStatus {
|
if proxy, getErr := h.adminService.GetProxy(ctx, existingID); getErr == nil && proxy != nil && proxy.Status != normalizedStatus {
|
||||||
_, _ = h.adminService.UpdateProxy(c.Request.Context(), existingID, &service.UpdateProxyInput{
|
_, _ = h.adminService.UpdateProxy(ctx, existingID, &service.UpdateProxyInput{
|
||||||
Status: normalizedStatus,
|
Status: normalizedStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -230,7 +236,7 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
created, err := h.adminService.CreateProxy(c.Request.Context(), &service.CreateProxyInput{
|
created, createErr := h.adminService.CreateProxy(ctx, &service.CreateProxyInput{
|
||||||
Name: defaultProxyName(item.Name),
|
Name: defaultProxyName(item.Name),
|
||||||
Protocol: item.Protocol,
|
Protocol: item.Protocol,
|
||||||
Host: item.Host,
|
Host: item.Host,
|
||||||
@@ -238,13 +244,13 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
|
|||||||
Username: item.Username,
|
Username: item.Username,
|
||||||
Password: item.Password,
|
Password: item.Password,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if createErr != nil {
|
||||||
result.ProxyFailed++
|
result.ProxyFailed++
|
||||||
result.Errors = append(result.Errors, DataImportError{
|
result.Errors = append(result.Errors, DataImportError{
|
||||||
Kind: "proxy",
|
Kind: "proxy",
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
ProxyKey: key,
|
ProxyKey: key,
|
||||||
Message: err.Error(),
|
Message: createErr.Error(),
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -252,7 +258,7 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
|
|||||||
result.ProxyCreated++
|
result.ProxyCreated++
|
||||||
|
|
||||||
if normalizedStatus != "" && normalizedStatus != created.Status {
|
if normalizedStatus != "" && normalizedStatus != created.Status {
|
||||||
_, _ = h.adminService.UpdateProxy(c.Request.Context(), created.ID, &service.UpdateProxyInput{
|
_, _ = h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{
|
||||||
Status: normalizedStatus,
|
Status: normalizedStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -303,7 +309,7 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
|
|||||||
SkipDefaultGroupBind: skipDefaultGroupBind,
|
SkipDefaultGroupBind: skipDefaultGroupBind,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.adminService.CreateAccount(c.Request.Context(), accountInput); err != nil {
|
if _, err := h.adminService.CreateAccount(ctx, accountInput); err != nil {
|
||||||
result.AccountFailed++
|
result.AccountFailed++
|
||||||
result.Errors = append(result.Errors, DataImportError{
|
result.Errors = append(result.Errors, DataImportError{
|
||||||
Kind: "account",
|
Kind: "account",
|
||||||
@@ -315,7 +321,7 @@ func (h *AccountHandler) ImportData(c *gin.Context) {
|
|||||||
result.AccountCreated++
|
result.AccountCreated++
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, result)
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, error) {
|
func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, error) {
|
||||||
@@ -341,7 +347,7 @@ func (h *AccountHandler) listAccountsFiltered(ctx context.Context, platform, acc
|
|||||||
pageSize := dataPageCap
|
pageSize := dataPageCap
|
||||||
var out []service.Account
|
var out []service.Account
|
||||||
for {
|
for {
|
||||||
items, total, err := h.adminService.ListAccounts(ctx, page, pageSize, platform, accountType, status, search)
|
items, total, err := h.adminService.ListAccounts(ctx, page, pageSize, platform, accountType, status, search, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -10,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/domain"
|
"github.com/Wei-Shaw/sub2api/internal/domain"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
@@ -133,6 +140,13 @@ type BulkUpdateAccountsRequest struct {
|
|||||||
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckMixedChannelRequest represents check mixed channel risk request
|
||||||
|
type CheckMixedChannelRequest struct {
|
||||||
|
Platform string `json:"platform" binding:"required"`
|
||||||
|
GroupIDs []int64 `json:"group_ids"`
|
||||||
|
AccountID *int64 `json:"account_id"`
|
||||||
|
}
|
||||||
|
|
||||||
// AccountWithConcurrency extends Account with real-time concurrency info
|
// AccountWithConcurrency extends Account with real-time concurrency info
|
||||||
type AccountWithConcurrency struct {
|
type AccountWithConcurrency struct {
|
||||||
*dto.Account
|
*dto.Account
|
||||||
@@ -142,6 +156,44 @@ type AccountWithConcurrency struct {
|
|||||||
ActiveSessions *int `json:"active_sessions,omitempty"` // 当前活跃会话数
|
ActiveSessions *int `json:"active_sessions,omitempty"` // 当前活跃会话数
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *AccountHandler) buildAccountResponseWithRuntime(ctx context.Context, account *service.Account) AccountWithConcurrency {
|
||||||
|
item := AccountWithConcurrency{
|
||||||
|
Account: dto.AccountFromService(account),
|
||||||
|
CurrentConcurrency: 0,
|
||||||
|
}
|
||||||
|
if account == nil {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.concurrencyService != nil {
|
||||||
|
if counts, err := h.concurrencyService.GetAccountConcurrencyBatch(ctx, []int64{account.ID}); err == nil {
|
||||||
|
item.CurrentConcurrency = counts[account.ID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.IsAnthropicOAuthOrSetupToken() {
|
||||||
|
if h.accountUsageService != nil && account.GetWindowCostLimit() > 0 {
|
||||||
|
startTime := account.GetCurrentWindowStartTime()
|
||||||
|
if stats, err := h.accountUsageService.GetAccountWindowStats(ctx, account.ID, startTime); err == nil && stats != nil {
|
||||||
|
cost := stats.StandardCost
|
||||||
|
item.CurrentWindowCost = &cost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.sessionLimitCache != nil && account.GetMaxSessions() > 0 {
|
||||||
|
idleTimeout := time.Duration(account.GetSessionIdleTimeoutMinutes()) * time.Minute
|
||||||
|
idleTimeouts := map[int64]time.Duration{account.ID: idleTimeout}
|
||||||
|
if sessions, err := h.sessionLimitCache.GetActiveSessionCountBatch(ctx, []int64{account.ID}, idleTimeouts); err == nil {
|
||||||
|
if count, ok := sessions[account.ID]; ok {
|
||||||
|
item.ActiveSessions = &count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
// List handles listing all accounts with pagination
|
// List handles listing all accounts with pagination
|
||||||
// GET /api/v1/admin/accounts
|
// GET /api/v1/admin/accounts
|
||||||
func (h *AccountHandler) List(c *gin.Context) {
|
func (h *AccountHandler) List(c *gin.Context) {
|
||||||
@@ -156,7 +208,12 @@ func (h *AccountHandler) List(c *gin.Context) {
|
|||||||
search = search[:100]
|
search = search[:100]
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, platform, accountType, status, search)
|
var groupID int64
|
||||||
|
if groupIDStr := c.Query("group"); groupIDStr != "" {
|
||||||
|
groupID, _ = strconv.ParseInt(groupIDStr, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, platform, accountType, status, search, groupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
@@ -257,9 +314,71 @@ func (h *AccountHandler) List(c *gin.Context) {
|
|||||||
result[i] = item
|
result[i] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
etag := buildAccountsListETag(result, total, page, pageSize, platform, accountType, status, search)
|
||||||
|
if etag != "" {
|
||||||
|
c.Header("ETag", etag)
|
||||||
|
c.Header("Vary", "If-None-Match")
|
||||||
|
if ifNoneMatchMatched(c.GetHeader("If-None-Match"), etag) {
|
||||||
|
c.Status(http.StatusNotModified)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response.Paginated(c, result, total, page, pageSize)
|
response.Paginated(c, result, total, page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildAccountsListETag(
|
||||||
|
items []AccountWithConcurrency,
|
||||||
|
total int64,
|
||||||
|
page, pageSize int,
|
||||||
|
platform, accountType, status, search string,
|
||||||
|
) string {
|
||||||
|
payload := struct {
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
AccountType string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Search string `json:"search"`
|
||||||
|
Items []AccountWithConcurrency `json:"items"`
|
||||||
|
}{
|
||||||
|
Total: total,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
Platform: platform,
|
||||||
|
AccountType: accountType,
|
||||||
|
Status: status,
|
||||||
|
Search: search,
|
||||||
|
Items: items,
|
||||||
|
}
|
||||||
|
raw, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256(raw)
|
||||||
|
return "\"" + hex.EncodeToString(sum[:]) + "\""
|
||||||
|
}
|
||||||
|
|
||||||
|
func ifNoneMatchMatched(ifNoneMatch, etag string) bool {
|
||||||
|
if etag == "" || ifNoneMatch == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, token := range strings.Split(ifNoneMatch, ",") {
|
||||||
|
candidate := strings.TrimSpace(token)
|
||||||
|
if candidate == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if candidate == etag {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(candidate, "W/") && strings.TrimPrefix(candidate, "W/") == etag {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// GetByID handles getting an account by ID
|
// GetByID handles getting an account by ID
|
||||||
// GET /api/v1/admin/accounts/:id
|
// GET /api/v1/admin/accounts/:id
|
||||||
func (h *AccountHandler) GetByID(c *gin.Context) {
|
func (h *AccountHandler) GetByID(c *gin.Context) {
|
||||||
@@ -275,7 +394,51 @@ func (h *AccountHandler) GetByID(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.AccountFromService(account))
|
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckMixedChannel handles checking mixed channel risk for account-group binding.
|
||||||
|
// POST /api/v1/admin/accounts/check-mixed-channel
|
||||||
|
func (h *AccountHandler) CheckMixedChannel(c *gin.Context) {
|
||||||
|
var req CheckMixedChannelRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.GroupIDs) == 0 {
|
||||||
|
response.Success(c, gin.H{"has_risk": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accountID := int64(0)
|
||||||
|
if req.AccountID != nil {
|
||||||
|
accountID = *req.AccountID
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.adminService.CheckMixedChannelRisk(c.Request.Context(), accountID, req.Platform, req.GroupIDs)
|
||||||
|
if err != nil {
|
||||||
|
var mixedErr *service.MixedChannelError
|
||||||
|
if errors.As(err, &mixedErr) {
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"has_risk": true,
|
||||||
|
"error": "mixed_channel_warning",
|
||||||
|
"message": mixedErr.Error(),
|
||||||
|
"details": gin.H{
|
||||||
|
"group_id": mixedErr.GroupID,
|
||||||
|
"group_name": mixedErr.GroupName,
|
||||||
|
"current_platform": mixedErr.CurrentPlatform,
|
||||||
|
"other_platform": mixedErr.OtherPlatform,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{"has_risk": false})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create handles creating a new account
|
// Create handles creating a new account
|
||||||
@@ -294,46 +457,51 @@ func (h *AccountHandler) Create(c *gin.Context) {
|
|||||||
// 确定是否跳过混合渠道检查
|
// 确定是否跳过混合渠道检查
|
||||||
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
|
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
|
||||||
|
|
||||||
account, err := h.adminService.CreateAccount(c.Request.Context(), &service.CreateAccountInput{
|
result, err := executeAdminIdempotent(c, "admin.accounts.create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
Name: req.Name,
|
account, execErr := h.adminService.CreateAccount(ctx, &service.CreateAccountInput{
|
||||||
Notes: req.Notes,
|
Name: req.Name,
|
||||||
Platform: req.Platform,
|
Notes: req.Notes,
|
||||||
Type: req.Type,
|
Platform: req.Platform,
|
||||||
Credentials: req.Credentials,
|
Type: req.Type,
|
||||||
Extra: req.Extra,
|
Credentials: req.Credentials,
|
||||||
ProxyID: req.ProxyID,
|
Extra: req.Extra,
|
||||||
Concurrency: req.Concurrency,
|
ProxyID: req.ProxyID,
|
||||||
Priority: req.Priority,
|
Concurrency: req.Concurrency,
|
||||||
RateMultiplier: req.RateMultiplier,
|
Priority: req.Priority,
|
||||||
GroupIDs: req.GroupIDs,
|
RateMultiplier: req.RateMultiplier,
|
||||||
ExpiresAt: req.ExpiresAt,
|
GroupIDs: req.GroupIDs,
|
||||||
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
ExpiresAt: req.ExpiresAt,
|
||||||
SkipMixedChannelCheck: skipCheck,
|
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
||||||
|
SkipMixedChannelCheck: skipCheck,
|
||||||
|
})
|
||||||
|
if execErr != nil {
|
||||||
|
return nil, execErr
|
||||||
|
}
|
||||||
|
return h.buildAccountResponseWithRuntime(ctx, account), nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 检查是否为混合渠道错误
|
// 检查是否为混合渠道错误
|
||||||
var mixedErr *service.MixedChannelError
|
var mixedErr *service.MixedChannelError
|
||||||
if errors.As(err, &mixedErr) {
|
if errors.As(err, &mixedErr) {
|
||||||
// 返回特殊错误码要求确认
|
// 创建接口仅返回最小必要字段,详细信息由专门检查接口提供
|
||||||
c.JSON(409, gin.H{
|
c.JSON(409, gin.H{
|
||||||
"error": "mixed_channel_warning",
|
"error": "mixed_channel_warning",
|
||||||
"message": mixedErr.Error(),
|
"message": mixedErr.Error(),
|
||||||
"details": gin.H{
|
|
||||||
"group_id": mixedErr.GroupID,
|
|
||||||
"group_name": mixedErr.GroupName,
|
|
||||||
"current_platform": mixedErr.CurrentPlatform,
|
|
||||||
"other_platform": mixedErr.OtherPlatform,
|
|
||||||
},
|
|
||||||
"require_confirmation": true,
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if retryAfter := service.RetryAfterSecondsFromError(err); retryAfter > 0 {
|
||||||
|
c.Header("Retry-After", strconv.Itoa(retryAfter))
|
||||||
|
}
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.AccountFromService(account))
|
if result != nil && result.Replayed {
|
||||||
|
c.Header("X-Idempotency-Replayed", "true")
|
||||||
|
}
|
||||||
|
response.Success(c, result.Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update handles updating an account
|
// Update handles updating an account
|
||||||
@@ -378,17 +546,10 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
|||||||
// 检查是否为混合渠道错误
|
// 检查是否为混合渠道错误
|
||||||
var mixedErr *service.MixedChannelError
|
var mixedErr *service.MixedChannelError
|
||||||
if errors.As(err, &mixedErr) {
|
if errors.As(err, &mixedErr) {
|
||||||
// 返回特殊错误码要求确认
|
// 更新接口仅返回最小必要字段,详细信息由专门检查接口提供
|
||||||
c.JSON(409, gin.H{
|
c.JSON(409, gin.H{
|
||||||
"error": "mixed_channel_warning",
|
"error": "mixed_channel_warning",
|
||||||
"message": mixedErr.Error(),
|
"message": mixedErr.Error(),
|
||||||
"details": gin.H{
|
|
||||||
"group_id": mixedErr.GroupID,
|
|
||||||
"group_name": mixedErr.GroupName,
|
|
||||||
"current_platform": mixedErr.CurrentPlatform,
|
|
||||||
"other_platform": mixedErr.OtherPlatform,
|
|
||||||
},
|
|
||||||
"require_confirmation": true,
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -397,7 +558,7 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.AccountFromService(account))
|
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete handles deleting an account
|
// Delete handles deleting an account
|
||||||
@@ -655,7 +816,7 @@ func (h *AccountHandler) Refresh(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.AccountFromService(updatedAccount))
|
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), updatedAccount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStats handles getting account statistics
|
// GetStats handles getting account statistics
|
||||||
@@ -713,7 +874,7 @@ func (h *AccountHandler) ClearError(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.AccountFromService(account))
|
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchCreate handles batch creating accounts
|
// BatchCreate handles batch creating accounts
|
||||||
@@ -727,61 +888,62 @@ func (h *AccountHandler) BatchCreate(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request.Context()
|
executeAdminIdempotentJSON(c, "admin.accounts.batch_create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
success := 0
|
success := 0
|
||||||
failed := 0
|
failed := 0
|
||||||
results := make([]gin.H, 0, len(req.Accounts))
|
results := make([]gin.H, 0, len(req.Accounts))
|
||||||
|
|
||||||
for _, item := range req.Accounts {
|
for _, item := range req.Accounts {
|
||||||
if item.RateMultiplier != nil && *item.RateMultiplier < 0 {
|
if item.RateMultiplier != nil && *item.RateMultiplier < 0 {
|
||||||
failed++
|
failed++
|
||||||
|
results = append(results, gin.H{
|
||||||
|
"name": item.Name,
|
||||||
|
"success": false,
|
||||||
|
"error": "rate_multiplier must be >= 0",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
skipCheck := item.ConfirmMixedChannelRisk != nil && *item.ConfirmMixedChannelRisk
|
||||||
|
|
||||||
|
account, err := h.adminService.CreateAccount(ctx, &service.CreateAccountInput{
|
||||||
|
Name: item.Name,
|
||||||
|
Notes: item.Notes,
|
||||||
|
Platform: item.Platform,
|
||||||
|
Type: item.Type,
|
||||||
|
Credentials: item.Credentials,
|
||||||
|
Extra: item.Extra,
|
||||||
|
ProxyID: item.ProxyID,
|
||||||
|
Concurrency: item.Concurrency,
|
||||||
|
Priority: item.Priority,
|
||||||
|
RateMultiplier: item.RateMultiplier,
|
||||||
|
GroupIDs: item.GroupIDs,
|
||||||
|
ExpiresAt: item.ExpiresAt,
|
||||||
|
AutoPauseOnExpired: item.AutoPauseOnExpired,
|
||||||
|
SkipMixedChannelCheck: skipCheck,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
failed++
|
||||||
|
results = append(results, gin.H{
|
||||||
|
"name": item.Name,
|
||||||
|
"success": false,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
success++
|
||||||
results = append(results, gin.H{
|
results = append(results, gin.H{
|
||||||
"name": item.Name,
|
"name": item.Name,
|
||||||
"success": false,
|
"id": account.ID,
|
||||||
"error": "rate_multiplier must be >= 0",
|
"success": true,
|
||||||
})
|
})
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
skipCheck := item.ConfirmMixedChannelRisk != nil && *item.ConfirmMixedChannelRisk
|
return gin.H{
|
||||||
|
"success": success,
|
||||||
account, err := h.adminService.CreateAccount(ctx, &service.CreateAccountInput{
|
"failed": failed,
|
||||||
Name: item.Name,
|
"results": results,
|
||||||
Notes: item.Notes,
|
}, nil
|
||||||
Platform: item.Platform,
|
|
||||||
Type: item.Type,
|
|
||||||
Credentials: item.Credentials,
|
|
||||||
Extra: item.Extra,
|
|
||||||
ProxyID: item.ProxyID,
|
|
||||||
Concurrency: item.Concurrency,
|
|
||||||
Priority: item.Priority,
|
|
||||||
RateMultiplier: item.RateMultiplier,
|
|
||||||
GroupIDs: item.GroupIDs,
|
|
||||||
ExpiresAt: item.ExpiresAt,
|
|
||||||
AutoPauseOnExpired: item.AutoPauseOnExpired,
|
|
||||||
SkipMixedChannelCheck: skipCheck,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
failed++
|
|
||||||
results = append(results, gin.H{
|
|
||||||
"name": item.Name,
|
|
||||||
"success": false,
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
success++
|
|
||||||
results = append(results, gin.H{
|
|
||||||
"name": item.Name,
|
|
||||||
"id": account.ID,
|
|
||||||
"success": true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Success(c, gin.H{
|
|
||||||
"success": success,
|
|
||||||
"failed": failed,
|
|
||||||
"results": results,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,57 +981,58 @@ func (h *AccountHandler) BatchUpdateCredentials(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
success := 0
|
|
||||||
failed := 0
|
|
||||||
results := []gin.H{}
|
|
||||||
|
|
||||||
|
// 阶段一:预验证所有账号存在,收集 credentials
|
||||||
|
type accountUpdate struct {
|
||||||
|
ID int64
|
||||||
|
Credentials map[string]any
|
||||||
|
}
|
||||||
|
updates := make([]accountUpdate, 0, len(req.AccountIDs))
|
||||||
for _, accountID := range req.AccountIDs {
|
for _, accountID := range req.AccountIDs {
|
||||||
// Get account
|
|
||||||
account, err := h.adminService.GetAccount(ctx, accountID)
|
account, err := h.adminService.GetAccount(ctx, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failed++
|
response.Error(c, 404, fmt.Sprintf("Account %d not found", accountID))
|
||||||
results = append(results, gin.H{
|
return
|
||||||
"account_id": accountID,
|
|
||||||
"success": false,
|
|
||||||
"error": "Account not found",
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update credentials field
|
|
||||||
if account.Credentials == nil {
|
if account.Credentials == nil {
|
||||||
account.Credentials = make(map[string]any)
|
account.Credentials = make(map[string]any)
|
||||||
}
|
}
|
||||||
|
|
||||||
account.Credentials[req.Field] = req.Value
|
account.Credentials[req.Field] = req.Value
|
||||||
|
updates = append(updates, accountUpdate{ID: accountID, Credentials: account.Credentials})
|
||||||
|
}
|
||||||
|
|
||||||
// Update account
|
// 阶段二:依次更新,返回每个账号的成功/失败明细,便于调用方重试
|
||||||
updateInput := &service.UpdateAccountInput{
|
success := 0
|
||||||
Credentials: account.Credentials,
|
failed := 0
|
||||||
}
|
successIDs := make([]int64, 0, len(updates))
|
||||||
|
failedIDs := make([]int64, 0, len(updates))
|
||||||
_, err = h.adminService.UpdateAccount(ctx, accountID, updateInput)
|
results := make([]gin.H, 0, len(updates))
|
||||||
if err != nil {
|
for _, u := range updates {
|
||||||
|
updateInput := &service.UpdateAccountInput{Credentials: u.Credentials}
|
||||||
|
if _, err := h.adminService.UpdateAccount(ctx, u.ID, updateInput); err != nil {
|
||||||
failed++
|
failed++
|
||||||
|
failedIDs = append(failedIDs, u.ID)
|
||||||
results = append(results, gin.H{
|
results = append(results, gin.H{
|
||||||
"account_id": accountID,
|
"account_id": u.ID,
|
||||||
"success": false,
|
"success": false,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
success++
|
success++
|
||||||
|
successIDs = append(successIDs, u.ID)
|
||||||
results = append(results, gin.H{
|
results = append(results, gin.H{
|
||||||
"account_id": accountID,
|
"account_id": u.ID,
|
||||||
"success": true,
|
"success": true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, gin.H{
|
response.Success(c, gin.H{
|
||||||
"success": success,
|
"success": success,
|
||||||
"failed": failed,
|
"failed": failed,
|
||||||
"results": results,
|
"success_ids": successIDs,
|
||||||
|
"failed_ids": failedIDs,
|
||||||
|
"results": results,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1104,7 +1267,13 @@ func (h *AccountHandler) ClearRateLimit(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, gin.H{"message": "Rate limit cleared successfully"})
|
account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTempUnschedulable handles getting temporary unschedulable status
|
// GetTempUnschedulable handles getting temporary unschedulable status
|
||||||
@@ -1194,7 +1363,7 @@ func (h *AccountHandler) SetSchedulable(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, dto.AccountFromService(account))
|
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAvailableModels handles getting available models for an account
|
// GetAvailableModels handles getting available models for an account
|
||||||
@@ -1291,32 +1460,14 @@ func (h *AccountHandler) GetAvailableModels(c *gin.Context) {
|
|||||||
|
|
||||||
// Handle Antigravity accounts: return Claude + Gemini models
|
// Handle Antigravity accounts: return Claude + Gemini models
|
||||||
if account.Platform == service.PlatformAntigravity {
|
if account.Platform == service.PlatformAntigravity {
|
||||||
// Antigravity 支持 Claude 和部分 Gemini 模型
|
// 直接复用 antigravity.DefaultModels(),与 /v1/models 端点保持同步
|
||||||
type UnifiedModel struct {
|
response.Success(c, antigravity.DefaultModels())
|
||||||
ID string `json:"id"`
|
return
|
||||||
Type string `json:"type"`
|
}
|
||||||
DisplayName string `json:"display_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var models []UnifiedModel
|
// Handle Sora accounts
|
||||||
|
if account.Platform == service.PlatformSora {
|
||||||
// 添加 Claude 模型
|
response.Success(c, service.DefaultSoraModels(nil))
|
||||||
for _, m := range claude.DefaultModels {
|
|
||||||
models = append(models, UnifiedModel{
|
|
||||||
ID: m.ID,
|
|
||||||
Type: m.Type,
|
|
||||||
DisplayName: m.DisplayName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加 Gemini 3 系列模型用于测试
|
|
||||||
geminiTestModels := []UnifiedModel{
|
|
||||||
{ID: "gemini-3-flash", Type: "model", DisplayName: "Gemini 3 Flash"},
|
|
||||||
{ID: "gemini-3-pro-preview", Type: "model", DisplayName: "Gemini 3 Pro Preview"},
|
|
||||||
}
|
|
||||||
models = append(models, geminiTestModels...)
|
|
||||||
|
|
||||||
response.Success(c, models)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1429,7 +1580,7 @@ func (h *AccountHandler) BatchRefreshTier(c *gin.Context) {
|
|||||||
accounts := make([]*service.Account, 0)
|
accounts := make([]*service.Account, 0)
|
||||||
|
|
||||||
if len(req.AccountIDs) == 0 {
|
if len(req.AccountIDs) == 0 {
|
||||||
allAccounts, _, err := h.adminService.ListAccounts(ctx, 1, 10000, "gemini", "oauth", "", "")
|
allAccounts, _, err := h.adminService.ListAccounts(ctx, 1, 10000, "gemini", "oauth", "", "", 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupAccountMixedChannelRouter(adminSvc *stubAdminService) *gin.Engine {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
accountHandler := NewAccountHandler(adminSvc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
router.POST("/api/v1/admin/accounts/check-mixed-channel", accountHandler.CheckMixedChannel)
|
||||||
|
router.POST("/api/v1/admin/accounts", accountHandler.Create)
|
||||||
|
router.PUT("/api/v1/admin/accounts/:id", accountHandler.Update)
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountHandlerCheckMixedChannelNoRisk(t *testing.T) {
|
||||||
|
adminSvc := newStubAdminService()
|
||||||
|
router := setupAccountMixedChannelRouter(adminSvc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"platform": "antigravity",
|
||||||
|
"group_ids": []int64{27},
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/check-mixed-channel", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
require.Equal(t, float64(0), resp["code"])
|
||||||
|
data, ok := resp["data"].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, false, data["has_risk"])
|
||||||
|
require.Equal(t, int64(0), adminSvc.lastMixedCheck.accountID)
|
||||||
|
require.Equal(t, "antigravity", adminSvc.lastMixedCheck.platform)
|
||||||
|
require.Equal(t, []int64{27}, adminSvc.lastMixedCheck.groupIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountHandlerCheckMixedChannelWithRisk(t *testing.T) {
|
||||||
|
adminSvc := newStubAdminService()
|
||||||
|
adminSvc.checkMixedErr = &service.MixedChannelError{
|
||||||
|
GroupID: 27,
|
||||||
|
GroupName: "claude-max",
|
||||||
|
CurrentPlatform: "Antigravity",
|
||||||
|
OtherPlatform: "Anthropic",
|
||||||
|
}
|
||||||
|
router := setupAccountMixedChannelRouter(adminSvc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"platform": "antigravity",
|
||||||
|
"group_ids": []int64{27},
|
||||||
|
"account_id": 99,
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/check-mixed-channel", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
require.Equal(t, float64(0), resp["code"])
|
||||||
|
data, ok := resp["data"].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, true, data["has_risk"])
|
||||||
|
require.Equal(t, "mixed_channel_warning", data["error"])
|
||||||
|
details, ok := data["details"].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, float64(27), details["group_id"])
|
||||||
|
require.Equal(t, "claude-max", details["group_name"])
|
||||||
|
require.Equal(t, "Antigravity", details["current_platform"])
|
||||||
|
require.Equal(t, "Anthropic", details["other_platform"])
|
||||||
|
require.Equal(t, int64(99), adminSvc.lastMixedCheck.accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountHandlerCreateMixedChannelConflictSimplifiedResponse(t *testing.T) {
|
||||||
|
adminSvc := newStubAdminService()
|
||||||
|
adminSvc.createAccountErr = &service.MixedChannelError{
|
||||||
|
GroupID: 27,
|
||||||
|
GroupName: "claude-max",
|
||||||
|
CurrentPlatform: "Antigravity",
|
||||||
|
OtherPlatform: "Anthropic",
|
||||||
|
}
|
||||||
|
router := setupAccountMixedChannelRouter(adminSvc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "ag-oauth-1",
|
||||||
|
"platform": "antigravity",
|
||||||
|
"type": "oauth",
|
||||||
|
"credentials": map[string]any{"refresh_token": "rt"},
|
||||||
|
"group_ids": []int64{27},
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusConflict, rec.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
require.Equal(t, "mixed_channel_warning", resp["error"])
|
||||||
|
require.Contains(t, resp["message"], "mixed_channel_warning")
|
||||||
|
_, hasDetails := resp["details"]
|
||||||
|
_, hasRequireConfirmation := resp["require_confirmation"]
|
||||||
|
require.False(t, hasDetails)
|
||||||
|
require.False(t, hasRequireConfirmation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountHandlerUpdateMixedChannelConflictSimplifiedResponse(t *testing.T) {
|
||||||
|
adminSvc := newStubAdminService()
|
||||||
|
adminSvc.updateAccountErr = &service.MixedChannelError{
|
||||||
|
GroupID: 27,
|
||||||
|
GroupName: "claude-max",
|
||||||
|
CurrentPlatform: "Antigravity",
|
||||||
|
OtherPlatform: "Anthropic",
|
||||||
|
}
|
||||||
|
router := setupAccountMixedChannelRouter(adminSvc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"group_ids": []int64{27},
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/accounts/3", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusConflict, rec.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
require.Equal(t, "mixed_channel_warning", resp["error"])
|
||||||
|
require.Contains(t, resp["message"], "mixed_channel_warning")
|
||||||
|
_, hasDetails := resp["details"]
|
||||||
|
_, hasRequireConfirmation := resp["require_confirmation"]
|
||||||
|
require.False(t, hasDetails)
|
||||||
|
require.False(t, hasRequireConfirmation)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAccountHandler_Create_AnthropicAPIKeyPassthroughExtraForwarded(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
|
adminSvc := newStubAdminService()
|
||||||
|
handler := NewAccountHandler(
|
||||||
|
adminSvc,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
router := gin.New()
|
||||||
|
router.POST("/api/v1/admin/accounts", handler.Create)
|
||||||
|
|
||||||
|
body := map[string]any{
|
||||||
|
"name": "anthropic-key-1",
|
||||||
|
"platform": "anthropic",
|
||||||
|
"type": "apikey",
|
||||||
|
"credentials": map[string]any{
|
||||||
|
"api_key": "sk-ant-xxx",
|
||||||
|
"base_url": "https://api.anthropic.com",
|
||||||
|
},
|
||||||
|
"extra": map[string]any{
|
||||||
|
"anthropic_passthrough": true,
|
||||||
|
},
|
||||||
|
"concurrency": 1,
|
||||||
|
"priority": 1,
|
||||||
|
}
|
||||||
|
raw, err := json.Marshal(body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts", bytes.NewReader(raw))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
require.Len(t, adminSvc.createdAccounts, 1)
|
||||||
|
|
||||||
|
created := adminSvc.createdAccounts[0]
|
||||||
|
require.Equal(t, "anthropic", created.Platform)
|
||||||
|
require.Equal(t, "apikey", created.Type)
|
||||||
|
require.NotNil(t, created.Extra)
|
||||||
|
require.Equal(t, true, created.Extra["anthropic_passthrough"])
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) {
|
|||||||
router.DELETE("/api/v1/admin/proxies/:id", proxyHandler.Delete)
|
router.DELETE("/api/v1/admin/proxies/:id", proxyHandler.Delete)
|
||||||
router.POST("/api/v1/admin/proxies/batch-delete", proxyHandler.BatchDelete)
|
router.POST("/api/v1/admin/proxies/batch-delete", proxyHandler.BatchDelete)
|
||||||
router.POST("/api/v1/admin/proxies/:id/test", proxyHandler.Test)
|
router.POST("/api/v1/admin/proxies/:id/test", proxyHandler.Test)
|
||||||
|
router.POST("/api/v1/admin/proxies/:id/quality-check", proxyHandler.CheckQuality)
|
||||||
router.GET("/api/v1/admin/proxies/:id/stats", proxyHandler.GetStats)
|
router.GET("/api/v1/admin/proxies/:id/stats", proxyHandler.GetStats)
|
||||||
router.GET("/api/v1/admin/proxies/:id/accounts", proxyHandler.GetProxyAccounts)
|
router.GET("/api/v1/admin/proxies/:id/accounts", proxyHandler.GetProxyAccounts)
|
||||||
|
|
||||||
@@ -208,6 +209,11 @@ func TestProxyHandlerEndpoints(t *testing.T) {
|
|||||||
router.ServeHTTP(rec, req)
|
router.ServeHTTP(rec, req)
|
||||||
require.Equal(t, http.StatusOK, rec.Code)
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/api/v1/admin/proxies/4/quality-check", nil)
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/4/stats", nil)
|
req = httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/4/stats", nil)
|
||||||
router.ServeHTTP(rec, req)
|
router.ServeHTTP(rec, req)
|
||||||
|
|||||||
@@ -58,6 +58,96 @@ func TestParseOpsDuration(t *testing.T) {
|
|||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseOpsOpenAITokenStatsDuration(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want time.Duration
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{input: "30m", want: 30 * time.Minute, ok: true},
|
||||||
|
{input: "1h", want: time.Hour, ok: true},
|
||||||
|
{input: "1d", want: 24 * time.Hour, ok: true},
|
||||||
|
{input: "15d", want: 15 * 24 * time.Hour, ok: true},
|
||||||
|
{input: "30d", want: 30 * 24 * time.Hour, ok: true},
|
||||||
|
{input: "7d", want: 0, ok: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
got, ok := parseOpsOpenAITokenStatsDuration(tt.input)
|
||||||
|
require.Equal(t, tt.ok, ok, "input=%s", tt.input)
|
||||||
|
require.Equal(t, tt.want, got, "input=%s", tt.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseOpsOpenAITokenStatsFilter_Defaults(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
|
||||||
|
before := time.Now().UTC()
|
||||||
|
filter, err := parseOpsOpenAITokenStatsFilter(c)
|
||||||
|
after := time.Now().UTC()
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, filter)
|
||||||
|
require.Equal(t, "30d", filter.TimeRange)
|
||||||
|
require.Equal(t, 1, filter.Page)
|
||||||
|
require.Equal(t, 20, filter.PageSize)
|
||||||
|
require.Equal(t, 0, filter.TopN)
|
||||||
|
require.Nil(t, filter.GroupID)
|
||||||
|
require.Equal(t, "", filter.Platform)
|
||||||
|
require.True(t, filter.StartTime.Before(filter.EndTime))
|
||||||
|
require.WithinDuration(t, before.Add(-30*24*time.Hour), filter.StartTime, 2*time.Second)
|
||||||
|
require.WithinDuration(t, after, filter.EndTime, 2*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseOpsOpenAITokenStatsFilter_WithTopN(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = httptest.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
"/?time_range=1h&platform=openai&group_id=12&top_n=50",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
filter, err := parseOpsOpenAITokenStatsFilter(c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "1h", filter.TimeRange)
|
||||||
|
require.Equal(t, "openai", filter.Platform)
|
||||||
|
require.NotNil(t, filter.GroupID)
|
||||||
|
require.Equal(t, int64(12), *filter.GroupID)
|
||||||
|
require.Equal(t, 50, filter.TopN)
|
||||||
|
require.Equal(t, 0, filter.Page)
|
||||||
|
require.Equal(t, 0, filter.PageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseOpsOpenAITokenStatsFilter_InvalidParams(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
"/?time_range=7d",
|
||||||
|
"/?group_id=0",
|
||||||
|
"/?group_id=abc",
|
||||||
|
"/?top_n=0",
|
||||||
|
"/?top_n=101",
|
||||||
|
"/?top_n=10&page=1",
|
||||||
|
"/?top_n=10&page_size=20",
|
||||||
|
"/?page=0",
|
||||||
|
"/?page_size=0",
|
||||||
|
"/?page_size=101",
|
||||||
|
}
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
for _, rawURL := range tests {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = httptest.NewRequest(http.MethodGet, rawURL, nil)
|
||||||
|
|
||||||
|
_, err := parseOpsOpenAITokenStatsFilter(c)
|
||||||
|
require.Error(t, err, "url=%s", rawURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseOpsTimeRange(t *testing.T) {
|
func TestParseOpsTimeRange(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|||||||
@@ -10,19 +10,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type stubAdminService struct {
|
type stubAdminService struct {
|
||||||
users []service.User
|
users []service.User
|
||||||
apiKeys []service.APIKey
|
apiKeys []service.APIKey
|
||||||
groups []service.Group
|
groups []service.Group
|
||||||
accounts []service.Account
|
accounts []service.Account
|
||||||
proxies []service.Proxy
|
proxies []service.Proxy
|
||||||
proxyCounts []service.ProxyWithAccountCount
|
proxyCounts []service.ProxyWithAccountCount
|
||||||
redeems []service.RedeemCode
|
redeems []service.RedeemCode
|
||||||
createdAccounts []*service.CreateAccountInput
|
createdAccounts []*service.CreateAccountInput
|
||||||
createdProxies []*service.CreateProxyInput
|
createdProxies []*service.CreateProxyInput
|
||||||
updatedProxyIDs []int64
|
updatedProxyIDs []int64
|
||||||
updatedProxies []*service.UpdateProxyInput
|
updatedProxies []*service.UpdateProxyInput
|
||||||
testedProxyIDs []int64
|
testedProxyIDs []int64
|
||||||
mu sync.Mutex
|
createAccountErr error
|
||||||
|
updateAccountErr error
|
||||||
|
checkMixedErr error
|
||||||
|
lastMixedCheck struct {
|
||||||
|
accountID int64
|
||||||
|
platform string
|
||||||
|
groupIDs []int64
|
||||||
|
}
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStubAdminService() *stubAdminService {
|
func newStubAdminService() *stubAdminService {
|
||||||
@@ -166,7 +174,7 @@ func (s *stubAdminService) GetGroupAPIKeys(ctx context.Context, groupID int64, p
|
|||||||
return s.apiKeys, int64(len(s.apiKeys)), nil
|
return s.apiKeys, int64(len(s.apiKeys)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubAdminService) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string) ([]service.Account, int64, error) {
|
func (s *stubAdminService) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64) ([]service.Account, int64, error) {
|
||||||
return s.accounts, int64(len(s.accounts)), nil
|
return s.accounts, int64(len(s.accounts)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,11 +196,17 @@ func (s *stubAdminService) CreateAccount(ctx context.Context, input *service.Cre
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.createdAccounts = append(s.createdAccounts, input)
|
s.createdAccounts = append(s.createdAccounts, input)
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
if s.createAccountErr != nil {
|
||||||
|
return nil, s.createAccountErr
|
||||||
|
}
|
||||||
account := service.Account{ID: 300, Name: input.Name, Status: service.StatusActive}
|
account := service.Account{ID: 300, Name: input.Name, Status: service.StatusActive}
|
||||||
return &account, nil
|
return &account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubAdminService) UpdateAccount(ctx context.Context, id int64, input *service.UpdateAccountInput) (*service.Account, error) {
|
func (s *stubAdminService) UpdateAccount(ctx context.Context, id int64, input *service.UpdateAccountInput) (*service.Account, error) {
|
||||||
|
if s.updateAccountErr != nil {
|
||||||
|
return nil, s.updateAccountErr
|
||||||
|
}
|
||||||
account := service.Account{ID: id, Name: input.Name, Status: service.StatusActive}
|
account := service.Account{ID: id, Name: input.Name, Status: service.StatusActive}
|
||||||
return &account, nil
|
return &account, nil
|
||||||
}
|
}
|
||||||
@@ -224,6 +238,13 @@ func (s *stubAdminService) BulkUpdateAccounts(ctx context.Context, input *servic
|
|||||||
return &service.BulkUpdateAccountsResult{Success: 1, Failed: 0, SuccessIDs: []int64{1}}, nil
|
return &service.BulkUpdateAccountsResult{Success: 1, Failed: 0, SuccessIDs: []int64{1}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubAdminService) CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error {
|
||||||
|
s.lastMixedCheck.accountID = currentAccountID
|
||||||
|
s.lastMixedCheck.platform = currentAccountPlatform
|
||||||
|
s.lastMixedCheck.groupIDs = append([]int64(nil), groupIDs...)
|
||||||
|
return s.checkMixedErr
|
||||||
|
}
|
||||||
|
|
||||||
func (s *stubAdminService) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]service.Proxy, int64, error) {
|
func (s *stubAdminService) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]service.Proxy, int64, error) {
|
||||||
search = strings.TrimSpace(strings.ToLower(search))
|
search = strings.TrimSpace(strings.ToLower(search))
|
||||||
filtered := make([]service.Proxy, 0, len(s.proxies))
|
filtered := make([]service.Proxy, 0, len(s.proxies))
|
||||||
@@ -327,6 +348,27 @@ func (s *stubAdminService) TestProxy(ctx context.Context, id int64) (*service.Pr
|
|||||||
return &service.ProxyTestResult{Success: true, Message: "ok"}, nil
|
return &service.ProxyTestResult{Success: true, Message: "ok"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubAdminService) CheckProxyQuality(ctx context.Context, id int64) (*service.ProxyQualityCheckResult, error) {
|
||||||
|
return &service.ProxyQualityCheckResult{
|
||||||
|
ProxyID: id,
|
||||||
|
Score: 95,
|
||||||
|
Grade: "A",
|
||||||
|
Summary: "通过 5 项,告警 0 项,失败 0 项,挑战 0 项",
|
||||||
|
PassedCount: 5,
|
||||||
|
WarnCount: 0,
|
||||||
|
FailedCount: 0,
|
||||||
|
ChallengeCount: 0,
|
||||||
|
CheckedAt: time.Now().Unix(),
|
||||||
|
Items: []service.ProxyQualityCheckItem{
|
||||||
|
{Target: "base_connectivity", Status: "pass", Message: "ok"},
|
||||||
|
{Target: "openai", Status: "pass", HTTPStatus: 401},
|
||||||
|
{Target: "anthropic", Status: "pass", HTTPStatus: 401},
|
||||||
|
{Target: "gemini", Status: "pass", HTTPStatus: 200},
|
||||||
|
{Target: "sora", Status: "pass", HTTPStatus: 401},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *stubAdminService) ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string) ([]service.RedeemCode, int64, error) {
|
func (s *stubAdminService) ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string) ([]service.RedeemCode, int64, error) {
|
||||||
return s.redeems, int64(len(s.redeems)), nil
|
return s.redeems, int64(len(s.redeems)), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,3 +65,27 @@ func (h *AntigravityOAuthHandler) ExchangeCode(c *gin.Context) {
|
|||||||
|
|
||||||
response.Success(c, tokenInfo)
|
response.Success(c, tokenInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AntigravityRefreshTokenRequest represents the request for validating Antigravity refresh token
|
||||||
|
type AntigravityRefreshTokenRequest struct {
|
||||||
|
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||||
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken validates an Antigravity refresh token and returns full token info
|
||||||
|
// POST /api/v1/admin/antigravity/oauth/refresh-token
|
||||||
|
func (h *AntigravityOAuthHandler) RefreshToken(c *gin.Context) {
|
||||||
|
var req AntigravityRefreshTokenRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "请求无效: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenInfo, err := h.antigravityOAuthService.ValidateRefreshToken(c.Request.Context(), req.RefreshToken, req.ProxyID)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, tokenInfo)
|
||||||
|
}
|
||||||
|
|||||||
208
backend/internal/handler/admin/batch_update_credentials_test.go
Normal file
208
backend/internal/handler/admin/batch_update_credentials_test.go
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
//go:build unit
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// failingAdminService 嵌入 stubAdminService,可配置 UpdateAccount 在指定 ID 时失败。
|
||||||
|
type failingAdminService struct {
|
||||||
|
*stubAdminService
|
||||||
|
failOnAccountID int64
|
||||||
|
updateCallCount atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *failingAdminService) UpdateAccount(ctx context.Context, id int64, input *service.UpdateAccountInput) (*service.Account, error) {
|
||||||
|
f.updateCallCount.Add(1)
|
||||||
|
if id == f.failOnAccountID {
|
||||||
|
return nil, errors.New("database error")
|
||||||
|
}
|
||||||
|
return f.stubAdminService.UpdateAccount(ctx, id, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupAccountHandlerWithService(adminSvc service.AdminService) (*gin.Engine, *AccountHandler) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
handler := NewAccountHandler(adminSvc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
router.POST("/api/v1/admin/accounts/batch-update-credentials", handler.BatchUpdateCredentials)
|
||||||
|
return router, handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatchUpdateCredentials_AllSuccess(t *testing.T) {
|
||||||
|
svc := &failingAdminService{stubAdminService: newStubAdminService()}
|
||||||
|
router, _ := setupAccountHandlerWithService(svc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(BatchUpdateCredentialsRequest{
|
||||||
|
AccountIDs: []int64{1, 2, 3},
|
||||||
|
Field: "account_uuid",
|
||||||
|
Value: "test-uuid",
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, w.Code, "全部成功时应返回 200")
|
||||||
|
require.Equal(t, int64(3), svc.updateCallCount.Load(), "应调用 3 次 UpdateAccount")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatchUpdateCredentials_PartialFailure(t *testing.T) {
|
||||||
|
// 让第 2 个账号(ID=2)更新时失败
|
||||||
|
svc := &failingAdminService{
|
||||||
|
stubAdminService: newStubAdminService(),
|
||||||
|
failOnAccountID: 2,
|
||||||
|
}
|
||||||
|
router, _ := setupAccountHandlerWithService(svc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(BatchUpdateCredentialsRequest{
|
||||||
|
AccountIDs: []int64{1, 2, 3},
|
||||||
|
Field: "org_uuid",
|
||||||
|
Value: "test-org",
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// 实现采用"部分成功"模式:总是返回 200 + 成功/失败明细
|
||||||
|
require.Equal(t, http.StatusOK, w.Code, "批量更新返回 200 + 成功/失败明细")
|
||||||
|
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||||
|
data := resp["data"].(map[string]any)
|
||||||
|
require.Equal(t, float64(2), data["success"], "应有 2 个成功")
|
||||||
|
require.Equal(t, float64(1), data["failed"], "应有 1 个失败")
|
||||||
|
|
||||||
|
// 所有 3 个账号都会被尝试更新(非 fail-fast)
|
||||||
|
require.Equal(t, int64(3), svc.updateCallCount.Load(),
|
||||||
|
"应调用 3 次 UpdateAccount(逐个尝试,失败后继续)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatchUpdateCredentials_FirstAccountNotFound(t *testing.T) {
|
||||||
|
// GetAccount 在 stubAdminService 中总是成功的,需要创建一个 GetAccount 会失败的 stub
|
||||||
|
svc := &getAccountFailingService{
|
||||||
|
stubAdminService: newStubAdminService(),
|
||||||
|
failOnAccountID: 1,
|
||||||
|
}
|
||||||
|
router, _ := setupAccountHandlerWithService(svc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(BatchUpdateCredentialsRequest{
|
||||||
|
AccountIDs: []int64{1, 2, 3},
|
||||||
|
Field: "account_uuid",
|
||||||
|
Value: "test",
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusNotFound, w.Code, "第一阶段验证失败应返回 404")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAccountFailingService 模拟 GetAccount 在特定 ID 时返回 not found。
|
||||||
|
type getAccountFailingService struct {
|
||||||
|
*stubAdminService
|
||||||
|
failOnAccountID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *getAccountFailingService) GetAccount(ctx context.Context, id int64) (*service.Account, error) {
|
||||||
|
if id == f.failOnAccountID {
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
return f.stubAdminService.GetAccount(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatchUpdateCredentials_InterceptWarmupRequests_NonBool(t *testing.T) {
|
||||||
|
svc := &failingAdminService{stubAdminService: newStubAdminService()}
|
||||||
|
router, _ := setupAccountHandlerWithService(svc)
|
||||||
|
|
||||||
|
// intercept_warmup_requests 传入非 bool 类型(string),应返回 400
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"account_ids": []int64{1},
|
||||||
|
"field": "intercept_warmup_requests",
|
||||||
|
"value": "not-a-bool",
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusBadRequest, w.Code,
|
||||||
|
"intercept_warmup_requests 传入非 bool 值应返回 400")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatchUpdateCredentials_InterceptWarmupRequests_ValidBool(t *testing.T) {
|
||||||
|
svc := &failingAdminService{stubAdminService: newStubAdminService()}
|
||||||
|
router, _ := setupAccountHandlerWithService(svc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"account_ids": []int64{1},
|
||||||
|
"field": "intercept_warmup_requests",
|
||||||
|
"value": true,
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, w.Code,
|
||||||
|
"intercept_warmup_requests 传入合法 bool 值应返回 200")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatchUpdateCredentials_AccountUUID_NonString(t *testing.T) {
|
||||||
|
svc := &failingAdminService{stubAdminService: newStubAdminService()}
|
||||||
|
router, _ := setupAccountHandlerWithService(svc)
|
||||||
|
|
||||||
|
// account_uuid 传入非 string 类型(number),应返回 400
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"account_ids": []int64{1},
|
||||||
|
"field": "account_uuid",
|
||||||
|
"value": 12345,
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusBadRequest, w.Code,
|
||||||
|
"account_uuid 传入非 string 值应返回 400")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatchUpdateCredentials_AccountUUID_NullValue(t *testing.T) {
|
||||||
|
svc := &failingAdminService{stubAdminService: newStubAdminService()}
|
||||||
|
router, _ := setupAccountHandlerWithService(svc)
|
||||||
|
|
||||||
|
// account_uuid 传入 null(设置为空),应正常通过
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"account_ids": []int64{1},
|
||||||
|
"field": "account_uuid",
|
||||||
|
"value": nil,
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("POST", "/api/v1/admin/accounts/batch-update-credentials", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, w.Code,
|
||||||
|
"account_uuid 传入 null 应返回 200")
|
||||||
|
}
|
||||||
@@ -379,7 +379,7 @@ func (h *DashboardHandler) GetBatchUsersUsage(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stats, err := h.dashboardService.GetBatchUserUsageStats(c.Request.Context(), req.UserIDs)
|
stats, err := h.dashboardService.GetBatchUserUsageStats(c.Request.Context(), req.UserIDs, time.Time{}, time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.Error(c, 500, "Failed to get user usage stats")
|
response.Error(c, 500, "Failed to get user usage stats")
|
||||||
return
|
return
|
||||||
@@ -407,7 +407,7 @@ func (h *DashboardHandler) GetBatchAPIKeysUsage(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stats, err := h.dashboardService.GetBatchAPIKeyUsageStats(c.Request.Context(), req.APIKeyIDs)
|
stats, err := h.dashboardService.GetBatchAPIKeyUsageStats(c.Request.Context(), req.APIKeyIDs, time.Time{}, time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.Error(c, 500, "Failed to get API key usage stats")
|
response.Error(c, 500, "Failed to get API key usage stats")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type CreateErrorPassthroughRuleRequest struct {
|
|||||||
ResponseCode *int `json:"response_code"`
|
ResponseCode *int `json:"response_code"`
|
||||||
PassthroughBody *bool `json:"passthrough_body"`
|
PassthroughBody *bool `json:"passthrough_body"`
|
||||||
CustomMessage *string `json:"custom_message"`
|
CustomMessage *string `json:"custom_message"`
|
||||||
|
SkipMonitoring *bool `json:"skip_monitoring"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ type UpdateErrorPassthroughRuleRequest struct {
|
|||||||
ResponseCode *int `json:"response_code"`
|
ResponseCode *int `json:"response_code"`
|
||||||
PassthroughBody *bool `json:"passthrough_body"`
|
PassthroughBody *bool `json:"passthrough_body"`
|
||||||
CustomMessage *string `json:"custom_message"`
|
CustomMessage *string `json:"custom_message"`
|
||||||
|
SkipMonitoring *bool `json:"skip_monitoring"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +124,9 @@ func (h *ErrorPassthroughHandler) Create(c *gin.Context) {
|
|||||||
} else {
|
} else {
|
||||||
rule.PassthroughBody = true
|
rule.PassthroughBody = true
|
||||||
}
|
}
|
||||||
|
if req.SkipMonitoring != nil {
|
||||||
|
rule.SkipMonitoring = *req.SkipMonitoring
|
||||||
|
}
|
||||||
rule.ResponseCode = req.ResponseCode
|
rule.ResponseCode = req.ResponseCode
|
||||||
rule.CustomMessage = req.CustomMessage
|
rule.CustomMessage = req.CustomMessage
|
||||||
rule.Description = req.Description
|
rule.Description = req.Description
|
||||||
@@ -190,6 +195,7 @@ func (h *ErrorPassthroughHandler) Update(c *gin.Context) {
|
|||||||
ResponseCode: existing.ResponseCode,
|
ResponseCode: existing.ResponseCode,
|
||||||
PassthroughBody: existing.PassthroughBody,
|
PassthroughBody: existing.PassthroughBody,
|
||||||
CustomMessage: existing.CustomMessage,
|
CustomMessage: existing.CustomMessage,
|
||||||
|
SkipMonitoring: existing.SkipMonitoring,
|
||||||
Description: existing.Description,
|
Description: existing.Description,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +236,9 @@ func (h *ErrorPassthroughHandler) Update(c *gin.Context) {
|
|||||||
if req.Description != nil {
|
if req.Description != nil {
|
||||||
rule.Description = req.Description
|
rule.Description = req.Description
|
||||||
}
|
}
|
||||||
|
if req.SkipMonitoring != nil {
|
||||||
|
rule.SkipMonitoring = *req.SkipMonitoring
|
||||||
|
}
|
||||||
|
|
||||||
// 确保切片不为 nil
|
// 确保切片不为 nil
|
||||||
if rule.ErrorCodes == nil {
|
if rule.ErrorCodes == nil {
|
||||||
|
|||||||
@@ -61,7 +61,11 @@ func (h *GeminiOAuthHandler) GenerateAuthURL(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
msg := err.Error()
|
msg := err.Error()
|
||||||
// Treat missing/invalid OAuth client configuration as a user/config error.
|
// Treat missing/invalid OAuth client configuration as a user/config error.
|
||||||
if strings.Contains(msg, "OAuth client not configured") || strings.Contains(msg, "requires your own OAuth Client") {
|
if strings.Contains(msg, "OAuth client not configured") ||
|
||||||
|
strings.Contains(msg, "requires your own OAuth Client") ||
|
||||||
|
strings.Contains(msg, "requires a custom OAuth Client") ||
|
||||||
|
strings.Contains(msg, "GEMINI_CLI_OAUTH_CLIENT_SECRET_MISSING") ||
|
||||||
|
strings.Contains(msg, "built-in Gemini CLI OAuth client_secret is not configured") {
|
||||||
response.BadRequest(c, "Failed to generate auth URL: "+msg)
|
response.BadRequest(c, "Failed to generate auth URL: "+msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func NewGroupHandler(adminService service.AdminService) *GroupHandler {
|
|||||||
type CreateGroupRequest struct {
|
type CreateGroupRequest struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity"`
|
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity sora"`
|
||||||
RateMultiplier float64 `json:"rate_multiplier"`
|
RateMultiplier float64 `json:"rate_multiplier"`
|
||||||
IsExclusive bool `json:"is_exclusive"`
|
IsExclusive bool `json:"is_exclusive"`
|
||||||
SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
|
SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"`
|
||||||
@@ -38,6 +38,10 @@ type CreateGroupRequest struct {
|
|||||||
ImagePrice1K *float64 `json:"image_price_1k"`
|
ImagePrice1K *float64 `json:"image_price_1k"`
|
||||||
ImagePrice2K *float64 `json:"image_price_2k"`
|
ImagePrice2K *float64 `json:"image_price_2k"`
|
||||||
ImagePrice4K *float64 `json:"image_price_4k"`
|
ImagePrice4K *float64 `json:"image_price_4k"`
|
||||||
|
SoraImagePrice360 *float64 `json:"sora_image_price_360"`
|
||||||
|
SoraImagePrice540 *float64 `json:"sora_image_price_540"`
|
||||||
|
SoraVideoPricePerRequest *float64 `json:"sora_video_price_per_request"`
|
||||||
|
SoraVideoPricePerRequestHD *float64 `json:"sora_video_price_per_request_hd"`
|
||||||
ClaudeCodeOnly bool `json:"claude_code_only"`
|
ClaudeCodeOnly bool `json:"claude_code_only"`
|
||||||
FallbackGroupID *int64 `json:"fallback_group_id"`
|
FallbackGroupID *int64 `json:"fallback_group_id"`
|
||||||
FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"`
|
FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"`
|
||||||
@@ -55,7 +59,7 @@ type CreateGroupRequest struct {
|
|||||||
type UpdateGroupRequest struct {
|
type UpdateGroupRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity"`
|
Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini antigravity sora"`
|
||||||
RateMultiplier *float64 `json:"rate_multiplier"`
|
RateMultiplier *float64 `json:"rate_multiplier"`
|
||||||
IsExclusive *bool `json:"is_exclusive"`
|
IsExclusive *bool `json:"is_exclusive"`
|
||||||
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
||||||
@@ -67,6 +71,10 @@ type UpdateGroupRequest struct {
|
|||||||
ImagePrice1K *float64 `json:"image_price_1k"`
|
ImagePrice1K *float64 `json:"image_price_1k"`
|
||||||
ImagePrice2K *float64 `json:"image_price_2k"`
|
ImagePrice2K *float64 `json:"image_price_2k"`
|
||||||
ImagePrice4K *float64 `json:"image_price_4k"`
|
ImagePrice4K *float64 `json:"image_price_4k"`
|
||||||
|
SoraImagePrice360 *float64 `json:"sora_image_price_360"`
|
||||||
|
SoraImagePrice540 *float64 `json:"sora_image_price_540"`
|
||||||
|
SoraVideoPricePerRequest *float64 `json:"sora_video_price_per_request"`
|
||||||
|
SoraVideoPricePerRequestHD *float64 `json:"sora_video_price_per_request_hd"`
|
||||||
ClaudeCodeOnly *bool `json:"claude_code_only"`
|
ClaudeCodeOnly *bool `json:"claude_code_only"`
|
||||||
FallbackGroupID *int64 `json:"fallback_group_id"`
|
FallbackGroupID *int64 `json:"fallback_group_id"`
|
||||||
FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"`
|
FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"`
|
||||||
@@ -179,6 +187,10 @@ func (h *GroupHandler) Create(c *gin.Context) {
|
|||||||
ImagePrice1K: req.ImagePrice1K,
|
ImagePrice1K: req.ImagePrice1K,
|
||||||
ImagePrice2K: req.ImagePrice2K,
|
ImagePrice2K: req.ImagePrice2K,
|
||||||
ImagePrice4K: req.ImagePrice4K,
|
ImagePrice4K: req.ImagePrice4K,
|
||||||
|
SoraImagePrice360: req.SoraImagePrice360,
|
||||||
|
SoraImagePrice540: req.SoraImagePrice540,
|
||||||
|
SoraVideoPricePerRequest: req.SoraVideoPricePerRequest,
|
||||||
|
SoraVideoPricePerRequestHD: req.SoraVideoPricePerRequestHD,
|
||||||
ClaudeCodeOnly: req.ClaudeCodeOnly,
|
ClaudeCodeOnly: req.ClaudeCodeOnly,
|
||||||
FallbackGroupID: req.FallbackGroupID,
|
FallbackGroupID: req.FallbackGroupID,
|
||||||
FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest,
|
FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest,
|
||||||
@@ -225,6 +237,10 @@ func (h *GroupHandler) Update(c *gin.Context) {
|
|||||||
ImagePrice1K: req.ImagePrice1K,
|
ImagePrice1K: req.ImagePrice1K,
|
||||||
ImagePrice2K: req.ImagePrice2K,
|
ImagePrice2K: req.ImagePrice2K,
|
||||||
ImagePrice4K: req.ImagePrice4K,
|
ImagePrice4K: req.ImagePrice4K,
|
||||||
|
SoraImagePrice360: req.SoraImagePrice360,
|
||||||
|
SoraImagePrice540: req.SoraImagePrice540,
|
||||||
|
SoraVideoPricePerRequest: req.SoraVideoPricePerRequest,
|
||||||
|
SoraVideoPricePerRequestHD: req.SoraVideoPricePerRequestHD,
|
||||||
ClaudeCodeOnly: req.ClaudeCodeOnly,
|
ClaudeCodeOnly: req.ClaudeCodeOnly,
|
||||||
FallbackGroupID: req.FallbackGroupID,
|
FallbackGroupID: req.FallbackGroupID,
|
||||||
FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest,
|
FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest,
|
||||||
|
|||||||
115
backend/internal/handler/admin/idempotency_helper.go
Normal file
115
backend/internal/handler/admin/idempotency_helper.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type idempotencyStoreUnavailableMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
idempotencyStoreUnavailableFailClose idempotencyStoreUnavailableMode = iota
|
||||||
|
idempotencyStoreUnavailableFailOpen
|
||||||
|
)
|
||||||
|
|
||||||
|
func executeAdminIdempotent(
|
||||||
|
c *gin.Context,
|
||||||
|
scope string,
|
||||||
|
payload any,
|
||||||
|
ttl time.Duration,
|
||||||
|
execute func(context.Context) (any, error),
|
||||||
|
) (*service.IdempotencyExecuteResult, error) {
|
||||||
|
coordinator := service.DefaultIdempotencyCoordinator()
|
||||||
|
if coordinator == nil {
|
||||||
|
data, err := execute(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &service.IdempotencyExecuteResult{Data: data}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actorScope := "admin:0"
|
||||||
|
if subject, ok := middleware2.GetAuthSubjectFromContext(c); ok {
|
||||||
|
actorScope = "admin:" + strconv.FormatInt(subject.UserID, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
return coordinator.Execute(c.Request.Context(), service.IdempotencyExecuteOptions{
|
||||||
|
Scope: scope,
|
||||||
|
ActorScope: actorScope,
|
||||||
|
Method: c.Request.Method,
|
||||||
|
Route: c.FullPath(),
|
||||||
|
IdempotencyKey: c.GetHeader("Idempotency-Key"),
|
||||||
|
Payload: payload,
|
||||||
|
RequireKey: true,
|
||||||
|
TTL: ttl,
|
||||||
|
}, execute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeAdminIdempotentJSON(
|
||||||
|
c *gin.Context,
|
||||||
|
scope string,
|
||||||
|
payload any,
|
||||||
|
ttl time.Duration,
|
||||||
|
execute func(context.Context) (any, error),
|
||||||
|
) {
|
||||||
|
executeAdminIdempotentJSONWithMode(c, scope, payload, ttl, idempotencyStoreUnavailableFailClose, execute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeAdminIdempotentJSONFailOpenOnStoreUnavailable(
|
||||||
|
c *gin.Context,
|
||||||
|
scope string,
|
||||||
|
payload any,
|
||||||
|
ttl time.Duration,
|
||||||
|
execute func(context.Context) (any, error),
|
||||||
|
) {
|
||||||
|
executeAdminIdempotentJSONWithMode(c, scope, payload, ttl, idempotencyStoreUnavailableFailOpen, execute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeAdminIdempotentJSONWithMode(
|
||||||
|
c *gin.Context,
|
||||||
|
scope string,
|
||||||
|
payload any,
|
||||||
|
ttl time.Duration,
|
||||||
|
mode idempotencyStoreUnavailableMode,
|
||||||
|
execute func(context.Context) (any, error),
|
||||||
|
) {
|
||||||
|
result, err := executeAdminIdempotent(c, scope, payload, ttl, execute)
|
||||||
|
if err != nil {
|
||||||
|
if infraerrors.Code(err) == infraerrors.Code(service.ErrIdempotencyStoreUnavail) {
|
||||||
|
strategy := "fail_close"
|
||||||
|
if mode == idempotencyStoreUnavailableFailOpen {
|
||||||
|
strategy = "fail_open"
|
||||||
|
}
|
||||||
|
service.RecordIdempotencyStoreUnavailable(c.FullPath(), scope, "handler_"+strategy)
|
||||||
|
logger.LegacyPrintf("handler.idempotency", "[Idempotency] store unavailable: method=%s route=%s scope=%s strategy=%s", c.Request.Method, c.FullPath(), scope, strategy)
|
||||||
|
if mode == idempotencyStoreUnavailableFailOpen {
|
||||||
|
data, fallbackErr := execute(c.Request.Context())
|
||||||
|
if fallbackErr != nil {
|
||||||
|
response.ErrorFrom(c, fallbackErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Header("X-Idempotency-Degraded", "store-unavailable")
|
||||||
|
response.Success(c, data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if retryAfter := service.RetryAfterSecondsFromError(err); retryAfter > 0 {
|
||||||
|
c.Header("Retry-After", strconv.Itoa(retryAfter))
|
||||||
|
}
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result != nil && result.Replayed {
|
||||||
|
c.Header("X-Idempotency-Replayed", "true")
|
||||||
|
}
|
||||||
|
response.Success(c, result.Data)
|
||||||
|
}
|
||||||
285
backend/internal/handler/admin/idempotency_helper_test.go
Normal file
285
backend/internal/handler/admin/idempotency_helper_test.go
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type storeUnavailableRepoStub struct{}
|
||||||
|
|
||||||
|
func (storeUnavailableRepoStub) CreateProcessing(context.Context, *service.IdempotencyRecord) (bool, error) {
|
||||||
|
return false, errors.New("store unavailable")
|
||||||
|
}
|
||||||
|
func (storeUnavailableRepoStub) GetByScopeAndKeyHash(context.Context, string, string) (*service.IdempotencyRecord, error) {
|
||||||
|
return nil, errors.New("store unavailable")
|
||||||
|
}
|
||||||
|
func (storeUnavailableRepoStub) TryReclaim(context.Context, int64, string, time.Time, time.Time, time.Time) (bool, error) {
|
||||||
|
return false, errors.New("store unavailable")
|
||||||
|
}
|
||||||
|
func (storeUnavailableRepoStub) ExtendProcessingLock(context.Context, int64, string, time.Time, time.Time) (bool, error) {
|
||||||
|
return false, errors.New("store unavailable")
|
||||||
|
}
|
||||||
|
func (storeUnavailableRepoStub) MarkSucceeded(context.Context, int64, int, string, time.Time) error {
|
||||||
|
return errors.New("store unavailable")
|
||||||
|
}
|
||||||
|
func (storeUnavailableRepoStub) MarkFailedRetryable(context.Context, int64, string, time.Time, time.Time) error {
|
||||||
|
return errors.New("store unavailable")
|
||||||
|
}
|
||||||
|
func (storeUnavailableRepoStub) DeleteExpired(context.Context, time.Time, int) (int64, error) {
|
||||||
|
return 0, errors.New("store unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteAdminIdempotentJSONFailCloseOnStoreUnavailable(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
service.SetDefaultIdempotencyCoordinator(service.NewIdempotencyCoordinator(storeUnavailableRepoStub{}, service.DefaultIdempotencyConfig()))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
service.SetDefaultIdempotencyCoordinator(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
var executed int
|
||||||
|
router := gin.New()
|
||||||
|
router.POST("/idempotent", func(c *gin.Context) {
|
||||||
|
executeAdminIdempotentJSON(c, "admin.test.high", map[string]any{"a": 1}, time.Minute, func(ctx context.Context) (any, error) {
|
||||||
|
executed++
|
||||||
|
return gin.H{"ok": true}, nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/idempotent", bytes.NewBufferString(`{"a":1}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Idempotency-Key", "test-key-1")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusServiceUnavailable, rec.Code)
|
||||||
|
require.Equal(t, 0, executed, "fail-close should block business execution when idempotency store is unavailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteAdminIdempotentJSONFailOpenOnStoreUnavailable(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
service.SetDefaultIdempotencyCoordinator(service.NewIdempotencyCoordinator(storeUnavailableRepoStub{}, service.DefaultIdempotencyConfig()))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
service.SetDefaultIdempotencyCoordinator(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
var executed int
|
||||||
|
router := gin.New()
|
||||||
|
router.POST("/idempotent", func(c *gin.Context) {
|
||||||
|
executeAdminIdempotentJSONFailOpenOnStoreUnavailable(c, "admin.test.medium", map[string]any{"a": 1}, time.Minute, func(ctx context.Context) (any, error) {
|
||||||
|
executed++
|
||||||
|
return gin.H{"ok": true}, nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/idempotent", bytes.NewBufferString(`{"a":1}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Idempotency-Key", "test-key-2")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
require.Equal(t, "store-unavailable", rec.Header().Get("X-Idempotency-Degraded"))
|
||||||
|
require.Equal(t, 1, executed, "fail-open strategy should allow semantic idempotent path to continue")
|
||||||
|
}
|
||||||
|
|
||||||
|
type memoryIdempotencyRepoStub struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
nextID int64
|
||||||
|
data map[string]*service.IdempotencyRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMemoryIdempotencyRepoStub() *memoryIdempotencyRepoStub {
|
||||||
|
return &memoryIdempotencyRepoStub{
|
||||||
|
nextID: 1,
|
||||||
|
data: make(map[string]*service.IdempotencyRecord),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryIdempotencyRepoStub) key(scope, keyHash string) string {
|
||||||
|
return scope + "|" + keyHash
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryIdempotencyRepoStub) clone(in *service.IdempotencyRecord) *service.IdempotencyRecord {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := *in
|
||||||
|
if in.LockedUntil != nil {
|
||||||
|
v := *in.LockedUntil
|
||||||
|
out.LockedUntil = &v
|
||||||
|
}
|
||||||
|
if in.ResponseBody != nil {
|
||||||
|
v := *in.ResponseBody
|
||||||
|
out.ResponseBody = &v
|
||||||
|
}
|
||||||
|
if in.ResponseStatus != nil {
|
||||||
|
v := *in.ResponseStatus
|
||||||
|
out.ResponseStatus = &v
|
||||||
|
}
|
||||||
|
if in.ErrorReason != nil {
|
||||||
|
v := *in.ErrorReason
|
||||||
|
out.ErrorReason = &v
|
||||||
|
}
|
||||||
|
return &out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryIdempotencyRepoStub) CreateProcessing(_ context.Context, record *service.IdempotencyRecord) (bool, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
k := r.key(record.Scope, record.IdempotencyKeyHash)
|
||||||
|
if _, ok := r.data[k]; ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
cp := r.clone(record)
|
||||||
|
cp.ID = r.nextID
|
||||||
|
r.nextID++
|
||||||
|
r.data[k] = cp
|
||||||
|
record.ID = cp.ID
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryIdempotencyRepoStub) GetByScopeAndKeyHash(_ context.Context, scope, keyHash string) (*service.IdempotencyRecord, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return r.clone(r.data[r.key(scope, keyHash)]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryIdempotencyRepoStub) TryReclaim(_ context.Context, id int64, fromStatus string, now, newLockedUntil, newExpiresAt time.Time) (bool, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
for _, rec := range r.data {
|
||||||
|
if rec.ID != id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rec.Status != fromStatus {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if rec.LockedUntil != nil && rec.LockedUntil.After(now) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
rec.Status = service.IdempotencyStatusProcessing
|
||||||
|
rec.LockedUntil = &newLockedUntil
|
||||||
|
rec.ExpiresAt = newExpiresAt
|
||||||
|
rec.ErrorReason = nil
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryIdempotencyRepoStub) ExtendProcessingLock(_ context.Context, id int64, requestFingerprint string, newLockedUntil, newExpiresAt time.Time) (bool, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
for _, rec := range r.data {
|
||||||
|
if rec.ID != id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rec.Status != service.IdempotencyStatusProcessing || rec.RequestFingerprint != requestFingerprint {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
rec.LockedUntil = &newLockedUntil
|
||||||
|
rec.ExpiresAt = newExpiresAt
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryIdempotencyRepoStub) MarkSucceeded(_ context.Context, id int64, responseStatus int, responseBody string, expiresAt time.Time) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
for _, rec := range r.data {
|
||||||
|
if rec.ID != id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rec.Status = service.IdempotencyStatusSucceeded
|
||||||
|
rec.LockedUntil = nil
|
||||||
|
rec.ExpiresAt = expiresAt
|
||||||
|
rec.ResponseStatus = &responseStatus
|
||||||
|
rec.ResponseBody = &responseBody
|
||||||
|
rec.ErrorReason = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryIdempotencyRepoStub) MarkFailedRetryable(_ context.Context, id int64, errorReason string, lockedUntil, expiresAt time.Time) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
for _, rec := range r.data {
|
||||||
|
if rec.ID != id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rec.Status = service.IdempotencyStatusFailedRetryable
|
||||||
|
rec.LockedUntil = &lockedUntil
|
||||||
|
rec.ExpiresAt = expiresAt
|
||||||
|
rec.ErrorReason = &errorReason
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *memoryIdempotencyRepoStub) DeleteExpired(_ context.Context, _ time.Time, _ int) (int64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteAdminIdempotentJSONConcurrentRetryOnlyOneSideEffect(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
repo := newMemoryIdempotencyRepoStub()
|
||||||
|
cfg := service.DefaultIdempotencyConfig()
|
||||||
|
cfg.ProcessingTimeout = 2 * time.Second
|
||||||
|
service.SetDefaultIdempotencyCoordinator(service.NewIdempotencyCoordinator(repo, cfg))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
service.SetDefaultIdempotencyCoordinator(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
var executed atomic.Int32
|
||||||
|
router := gin.New()
|
||||||
|
router.POST("/idempotent", func(c *gin.Context) {
|
||||||
|
executeAdminIdempotentJSON(c, "admin.test.concurrent", map[string]any{"a": 1}, time.Minute, func(ctx context.Context) (any, error) {
|
||||||
|
executed.Add(1)
|
||||||
|
time.Sleep(120 * time.Millisecond)
|
||||||
|
return gin.H{"ok": true}, nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
call := func() (int, http.Header) {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/idempotent", bytes.NewBufferString(`{"a":1}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Idempotency-Key", "same-key")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
return rec.Code, rec.Header()
|
||||||
|
}
|
||||||
|
|
||||||
|
var status1, status2 int
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
status1, _ = call()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
status2, _ = call()
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
require.Contains(t, []int{http.StatusOK, http.StatusConflict}, status1)
|
||||||
|
require.Contains(t, []int{http.StatusOK, http.StatusConflict}, status2)
|
||||||
|
require.Equal(t, int32(1), executed.Load(), "same idempotency key should execute side-effect only once")
|
||||||
|
|
||||||
|
status3, headers3 := call()
|
||||||
|
require.Equal(t, http.StatusOK, status3)
|
||||||
|
require.Equal(t, "true", headers3.Get("X-Idempotency-Replayed"))
|
||||||
|
require.Equal(t, int32(1), executed.Load())
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package admin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
@@ -16,6 +17,13 @@ type OpenAIOAuthHandler struct {
|
|||||||
adminService service.AdminService
|
adminService service.AdminService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func oauthPlatformFromPath(c *gin.Context) string {
|
||||||
|
if strings.Contains(c.FullPath(), "/admin/sora/") {
|
||||||
|
return service.PlatformSora
|
||||||
|
}
|
||||||
|
return service.PlatformOpenAI
|
||||||
|
}
|
||||||
|
|
||||||
// NewOpenAIOAuthHandler creates a new OpenAI OAuth handler
|
// NewOpenAIOAuthHandler creates a new OpenAI OAuth handler
|
||||||
func NewOpenAIOAuthHandler(openaiOAuthService *service.OpenAIOAuthService, adminService service.AdminService) *OpenAIOAuthHandler {
|
func NewOpenAIOAuthHandler(openaiOAuthService *service.OpenAIOAuthService, adminService service.AdminService) *OpenAIOAuthHandler {
|
||||||
return &OpenAIOAuthHandler{
|
return &OpenAIOAuthHandler{
|
||||||
@@ -52,6 +60,7 @@ func (h *OpenAIOAuthHandler) GenerateAuthURL(c *gin.Context) {
|
|||||||
type OpenAIExchangeCodeRequest struct {
|
type OpenAIExchangeCodeRequest struct {
|
||||||
SessionID string `json:"session_id" binding:"required"`
|
SessionID string `json:"session_id" binding:"required"`
|
||||||
Code string `json:"code" binding:"required"`
|
Code string `json:"code" binding:"required"`
|
||||||
|
State string `json:"state" binding:"required"`
|
||||||
RedirectURI string `json:"redirect_uri"`
|
RedirectURI string `json:"redirect_uri"`
|
||||||
ProxyID *int64 `json:"proxy_id"`
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
}
|
}
|
||||||
@@ -68,6 +77,7 @@ func (h *OpenAIOAuthHandler) ExchangeCode(c *gin.Context) {
|
|||||||
tokenInfo, err := h.openaiOAuthService.ExchangeCode(c.Request.Context(), &service.OpenAIExchangeCodeInput{
|
tokenInfo, err := h.openaiOAuthService.ExchangeCode(c.Request.Context(), &service.OpenAIExchangeCodeInput{
|
||||||
SessionID: req.SessionID,
|
SessionID: req.SessionID,
|
||||||
Code: req.Code,
|
Code: req.Code,
|
||||||
|
State: req.State,
|
||||||
RedirectURI: req.RedirectURI,
|
RedirectURI: req.RedirectURI,
|
||||||
ProxyID: req.ProxyID,
|
ProxyID: req.ProxyID,
|
||||||
})
|
})
|
||||||
@@ -81,18 +91,29 @@ func (h *OpenAIOAuthHandler) ExchangeCode(c *gin.Context) {
|
|||||||
|
|
||||||
// OpenAIRefreshTokenRequest represents the request for refreshing OpenAI token
|
// OpenAIRefreshTokenRequest represents the request for refreshing OpenAI token
|
||||||
type OpenAIRefreshTokenRequest struct {
|
type OpenAIRefreshTokenRequest struct {
|
||||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
RT string `json:"rt"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
ProxyID *int64 `json:"proxy_id"`
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshToken refreshes an OpenAI OAuth token
|
// RefreshToken refreshes an OpenAI OAuth token
|
||||||
// POST /api/v1/admin/openai/refresh-token
|
// POST /api/v1/admin/openai/refresh-token
|
||||||
|
// POST /api/v1/admin/sora/rt2at
|
||||||
func (h *OpenAIOAuthHandler) RefreshToken(c *gin.Context) {
|
func (h *OpenAIOAuthHandler) RefreshToken(c *gin.Context) {
|
||||||
var req OpenAIRefreshTokenRequest
|
var req OpenAIRefreshTokenRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
response.BadRequest(c, "Invalid request: "+err.Error())
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
refreshToken := strings.TrimSpace(req.RefreshToken)
|
||||||
|
if refreshToken == "" {
|
||||||
|
refreshToken = strings.TrimSpace(req.RT)
|
||||||
|
}
|
||||||
|
if refreshToken == "" {
|
||||||
|
response.BadRequest(c, "refresh_token is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var proxyURL string
|
var proxyURL string
|
||||||
if req.ProxyID != nil {
|
if req.ProxyID != nil {
|
||||||
@@ -102,7 +123,7 @@ func (h *OpenAIOAuthHandler) RefreshToken(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenInfo, err := h.openaiOAuthService.RefreshToken(c.Request.Context(), req.RefreshToken, proxyURL)
|
tokenInfo, err := h.openaiOAuthService.RefreshTokenWithClientID(c.Request.Context(), refreshToken, proxyURL, strings.TrimSpace(req.ClientID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
@@ -111,8 +132,39 @@ func (h *OpenAIOAuthHandler) RefreshToken(c *gin.Context) {
|
|||||||
response.Success(c, tokenInfo)
|
response.Success(c, tokenInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshAccountToken refreshes token for a specific OpenAI account
|
// ExchangeSoraSessionToken exchanges Sora session token to access token
|
||||||
|
// POST /api/v1/admin/sora/st2at
|
||||||
|
func (h *OpenAIOAuthHandler) ExchangeSoraSessionToken(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
SessionToken string `json:"session_token"`
|
||||||
|
ST string `json:"st"`
|
||||||
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionToken := strings.TrimSpace(req.SessionToken)
|
||||||
|
if sessionToken == "" {
|
||||||
|
sessionToken = strings.TrimSpace(req.ST)
|
||||||
|
}
|
||||||
|
if sessionToken == "" {
|
||||||
|
response.BadRequest(c, "session_token is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenInfo, err := h.openaiOAuthService.ExchangeSoraSessionToken(c.Request.Context(), sessionToken, req.ProxyID)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, tokenInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshAccountToken refreshes token for a specific OpenAI/Sora account
|
||||||
// POST /api/v1/admin/openai/accounts/:id/refresh
|
// POST /api/v1/admin/openai/accounts/:id/refresh
|
||||||
|
// POST /api/v1/admin/sora/accounts/:id/refresh
|
||||||
func (h *OpenAIOAuthHandler) RefreshAccountToken(c *gin.Context) {
|
func (h *OpenAIOAuthHandler) RefreshAccountToken(c *gin.Context) {
|
||||||
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -127,9 +179,9 @@ func (h *OpenAIOAuthHandler) RefreshAccountToken(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure account is OpenAI platform
|
platform := oauthPlatformFromPath(c)
|
||||||
if !account.IsOpenAI() {
|
if account.Platform != platform {
|
||||||
response.BadRequest(c, "Account is not an OpenAI account")
|
response.BadRequest(c, "Account platform does not match OAuth endpoint")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,12 +219,14 @@ func (h *OpenAIOAuthHandler) RefreshAccountToken(c *gin.Context) {
|
|||||||
response.Success(c, dto.AccountFromService(updatedAccount))
|
response.Success(c, dto.AccountFromService(updatedAccount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAccountFromOAuth creates a new OpenAI OAuth account from token info
|
// CreateAccountFromOAuth creates a new OpenAI/Sora OAuth account from token info
|
||||||
// POST /api/v1/admin/openai/create-from-oauth
|
// POST /api/v1/admin/openai/create-from-oauth
|
||||||
|
// POST /api/v1/admin/sora/create-from-oauth
|
||||||
func (h *OpenAIOAuthHandler) CreateAccountFromOAuth(c *gin.Context) {
|
func (h *OpenAIOAuthHandler) CreateAccountFromOAuth(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
SessionID string `json:"session_id" binding:"required"`
|
SessionID string `json:"session_id" binding:"required"`
|
||||||
Code string `json:"code" binding:"required"`
|
Code string `json:"code" binding:"required"`
|
||||||
|
State string `json:"state" binding:"required"`
|
||||||
RedirectURI string `json:"redirect_uri"`
|
RedirectURI string `json:"redirect_uri"`
|
||||||
ProxyID *int64 `json:"proxy_id"`
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -189,6 +243,7 @@ func (h *OpenAIOAuthHandler) CreateAccountFromOAuth(c *gin.Context) {
|
|||||||
tokenInfo, err := h.openaiOAuthService.ExchangeCode(c.Request.Context(), &service.OpenAIExchangeCodeInput{
|
tokenInfo, err := h.openaiOAuthService.ExchangeCode(c.Request.Context(), &service.OpenAIExchangeCodeInput{
|
||||||
SessionID: req.SessionID,
|
SessionID: req.SessionID,
|
||||||
Code: req.Code,
|
Code: req.Code,
|
||||||
|
State: req.State,
|
||||||
RedirectURI: req.RedirectURI,
|
RedirectURI: req.RedirectURI,
|
||||||
ProxyID: req.ProxyID,
|
ProxyID: req.ProxyID,
|
||||||
})
|
})
|
||||||
@@ -200,19 +255,25 @@ func (h *OpenAIOAuthHandler) CreateAccountFromOAuth(c *gin.Context) {
|
|||||||
// Build credentials from token info
|
// Build credentials from token info
|
||||||
credentials := h.openaiOAuthService.BuildAccountCredentials(tokenInfo)
|
credentials := h.openaiOAuthService.BuildAccountCredentials(tokenInfo)
|
||||||
|
|
||||||
|
platform := oauthPlatformFromPath(c)
|
||||||
|
|
||||||
// Use email as default name if not provided
|
// Use email as default name if not provided
|
||||||
name := req.Name
|
name := req.Name
|
||||||
if name == "" && tokenInfo.Email != "" {
|
if name == "" && tokenInfo.Email != "" {
|
||||||
name = tokenInfo.Email
|
name = tokenInfo.Email
|
||||||
}
|
}
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = "OpenAI OAuth Account"
|
if platform == service.PlatformSora {
|
||||||
|
name = "Sora OAuth Account"
|
||||||
|
} else {
|
||||||
|
name = "OpenAI OAuth Account"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create account
|
// Create account
|
||||||
account, err := h.adminService.CreateAccount(c.Request.Context(), &service.CreateAccountInput{
|
account, err := h.adminService.CreateAccount(c.Request.Context(), &service.CreateAccountInput{
|
||||||
Name: name,
|
Name: name,
|
||||||
Platform: "openai",
|
Platform: platform,
|
||||||
Type: "oauth",
|
Type: "oauth",
|
||||||
Credentials: credentials,
|
Credentials: credentials,
|
||||||
ProxyID: req.ProxyID,
|
ProxyID: req.ProxyID,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -218,6 +219,115 @@ func (h *OpsHandler) GetDashboardErrorDistribution(c *gin.Context) {
|
|||||||
response.Success(c, data)
|
response.Success(c, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDashboardOpenAITokenStats returns OpenAI token efficiency stats grouped by model.
|
||||||
|
// GET /api/v1/admin/ops/dashboard/openai-token-stats
|
||||||
|
func (h *OpsHandler) GetDashboardOpenAITokenStats(c *gin.Context) {
|
||||||
|
if h.opsService == nil {
|
||||||
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filter, err := parseOpsOpenAITokenStatsFilter(c)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := h.opsService.GetOpenAITokenStats(c.Request.Context(), filter)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOpsOpenAITokenStatsFilter(c *gin.Context) (*service.OpsOpenAITokenStatsFilter, error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("invalid request")
|
||||||
|
}
|
||||||
|
|
||||||
|
timeRange := strings.TrimSpace(c.Query("time_range"))
|
||||||
|
if timeRange == "" {
|
||||||
|
timeRange = "30d"
|
||||||
|
}
|
||||||
|
dur, ok := parseOpsOpenAITokenStatsDuration(timeRange)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid time_range")
|
||||||
|
}
|
||||||
|
end := time.Now().UTC()
|
||||||
|
start := end.Add(-dur)
|
||||||
|
|
||||||
|
filter := &service.OpsOpenAITokenStatsFilter{
|
||||||
|
TimeRange: timeRange,
|
||||||
|
StartTime: start,
|
||||||
|
EndTime: end,
|
||||||
|
Platform: strings.TrimSpace(c.Query("platform")),
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
|
||||||
|
id, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid group_id")
|
||||||
|
}
|
||||||
|
filter.GroupID = &id
|
||||||
|
}
|
||||||
|
|
||||||
|
topNRaw := strings.TrimSpace(c.Query("top_n"))
|
||||||
|
pageRaw := strings.TrimSpace(c.Query("page"))
|
||||||
|
pageSizeRaw := strings.TrimSpace(c.Query("page_size"))
|
||||||
|
if topNRaw != "" && (pageRaw != "" || pageSizeRaw != "") {
|
||||||
|
return nil, fmt.Errorf("invalid query: top_n cannot be used with page/page_size")
|
||||||
|
}
|
||||||
|
|
||||||
|
if topNRaw != "" {
|
||||||
|
topN, err := strconv.Atoi(topNRaw)
|
||||||
|
if err != nil || topN < 1 || topN > 100 {
|
||||||
|
return nil, fmt.Errorf("invalid top_n")
|
||||||
|
}
|
||||||
|
filter.TopN = topN
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.Page = 1
|
||||||
|
filter.PageSize = 20
|
||||||
|
if pageRaw != "" {
|
||||||
|
page, err := strconv.Atoi(pageRaw)
|
||||||
|
if err != nil || page < 1 {
|
||||||
|
return nil, fmt.Errorf("invalid page")
|
||||||
|
}
|
||||||
|
filter.Page = page
|
||||||
|
}
|
||||||
|
if pageSizeRaw != "" {
|
||||||
|
pageSize, err := strconv.Atoi(pageSizeRaw)
|
||||||
|
if err != nil || pageSize < 1 || pageSize > 100 {
|
||||||
|
return nil, fmt.Errorf("invalid page_size")
|
||||||
|
}
|
||||||
|
filter.PageSize = pageSize
|
||||||
|
}
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOpsOpenAITokenStatsDuration(v string) (time.Duration, bool) {
|
||||||
|
switch strings.TrimSpace(v) {
|
||||||
|
case "30m":
|
||||||
|
return 30 * time.Minute, true
|
||||||
|
case "1h":
|
||||||
|
return time.Hour, true
|
||||||
|
case "1d":
|
||||||
|
return 24 * time.Hour, true
|
||||||
|
case "15d":
|
||||||
|
return 15 * 24 * time.Hour, true
|
||||||
|
case "30d":
|
||||||
|
return 30 * 24 * time.Hour, true
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func pickThroughputBucketSeconds(window time.Duration) int {
|
func pickThroughputBucketSeconds(window time.Duration) int {
|
||||||
// Keep buckets predictable and avoid huge responses.
|
// Keep buckets predictable and avoid huge responses.
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testSettingRepo struct {
|
||||||
|
values map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestSettingRepo() *testSettingRepo {
|
||||||
|
return &testSettingRepo{values: map[string]string{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testSettingRepo) Get(ctx context.Context, key string) (*service.Setting, error) {
|
||||||
|
v, err := s.GetValue(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &service.Setting{Key: key, Value: v}, nil
|
||||||
|
}
|
||||||
|
func (s *testSettingRepo) GetValue(ctx context.Context, key string) (string, error) {
|
||||||
|
v, ok := s.values[key]
|
||||||
|
if !ok {
|
||||||
|
return "", service.ErrSettingNotFound
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
func (s *testSettingRepo) Set(ctx context.Context, key, value string) error {
|
||||||
|
s.values[key] = value
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *testSettingRepo) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
|
||||||
|
out := make(map[string]string, len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
if v, ok := s.values[k]; ok {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
func (s *testSettingRepo) SetMultiple(ctx context.Context, settings map[string]string) error {
|
||||||
|
for k, v := range settings {
|
||||||
|
s.values[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *testSettingRepo) GetAll(ctx context.Context) (map[string]string, error) {
|
||||||
|
out := make(map[string]string, len(s.values))
|
||||||
|
for k, v := range s.values {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
func (s *testSettingRepo) Delete(ctx context.Context, key string) error {
|
||||||
|
delete(s.values, key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOpsRuntimeRouter(handler *OpsHandler, withUser bool) *gin.Engine {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.New()
|
||||||
|
if withUser {
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{UserID: 7})
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
r.GET("/runtime/logging", handler.GetRuntimeLogConfig)
|
||||||
|
r.PUT("/runtime/logging", handler.UpdateRuntimeLogConfig)
|
||||||
|
r.POST("/runtime/logging/reset", handler.ResetRuntimeLogConfig)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuntimeOpsService(t *testing.T) *service.OpsService {
|
||||||
|
t.Helper()
|
||||||
|
if err := logger.Init(logger.InitOptions{
|
||||||
|
Level: "info",
|
||||||
|
Format: "json",
|
||||||
|
ServiceName: "sub2api",
|
||||||
|
Environment: "test",
|
||||||
|
Output: logger.OutputOptions{
|
||||||
|
ToStdout: false,
|
||||||
|
ToFile: false,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("init logger: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settingRepo := newTestSettingRepo()
|
||||||
|
cfg := &config.Config{
|
||||||
|
Ops: config.OpsConfig{Enabled: true},
|
||||||
|
Log: config.LogConfig{
|
||||||
|
Level: "info",
|
||||||
|
Caller: true,
|
||||||
|
StacktraceLevel: "error",
|
||||||
|
Sampling: config.LogSamplingConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Initial: 100,
|
||||||
|
Thereafter: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return service.NewOpsService(nil, settingRepo, cfg, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsRuntimeLoggingHandler_GetConfig(t *testing.T) {
|
||||||
|
h := NewOpsHandler(newRuntimeOpsService(t))
|
||||||
|
r := newOpsRuntimeRouter(h, false)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/runtime/logging", nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status=%d, want 200", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsRuntimeLoggingHandler_UpdateUnauthorized(t *testing.T) {
|
||||||
|
h := NewOpsHandler(newRuntimeOpsService(t))
|
||||||
|
r := newOpsRuntimeRouter(h, false)
|
||||||
|
|
||||||
|
body := `{"level":"debug","enable_sampling":false,"sampling_initial":100,"sampling_thereafter":100,"caller":true,"stacktrace_level":"error","retention_days":30}`
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/runtime/logging", bytes.NewBufferString(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("status=%d, want 401", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsRuntimeLoggingHandler_UpdateAndResetSuccess(t *testing.T) {
|
||||||
|
h := NewOpsHandler(newRuntimeOpsService(t))
|
||||||
|
r := newOpsRuntimeRouter(h, true)
|
||||||
|
|
||||||
|
payload := map[string]any{
|
||||||
|
"level": "debug",
|
||||||
|
"enable_sampling": false,
|
||||||
|
"sampling_initial": 100,
|
||||||
|
"sampling_thereafter": 100,
|
||||||
|
"caller": true,
|
||||||
|
"stacktrace_level": "error",
|
||||||
|
"retention_days": 30,
|
||||||
|
}
|
||||||
|
raw, _ := json.Marshal(payload)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/runtime/logging", bytes.NewReader(raw))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("update status=%d, want 200, body=%s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/runtime/logging/reset", nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("reset status=%d, want 200, body=%s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -101,6 +102,84 @@ func (h *OpsHandler) UpdateAlertRuntimeSettings(c *gin.Context) {
|
|||||||
response.Success(c, updated)
|
response.Success(c, updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRuntimeLogConfig returns runtime log config (DB-backed).
|
||||||
|
// GET /api/v1/admin/ops/runtime/logging
|
||||||
|
func (h *OpsHandler) GetRuntimeLogConfig(c *gin.Context) {
|
||||||
|
if h.opsService == nil {
|
||||||
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := h.opsService.GetRuntimeLogConfig(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, http.StatusInternalServerError, "Failed to get runtime log config")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRuntimeLogConfig updates runtime log config and applies changes immediately.
|
||||||
|
// PUT /api/v1/admin/ops/runtime/logging
|
||||||
|
func (h *OpsHandler) UpdateRuntimeLogConfig(c *gin.Context) {
|
||||||
|
if h.opsService == nil {
|
||||||
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req service.OpsRuntimeLogConfig
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "Invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subject, ok := middleware.GetAuthSubjectFromContext(c)
|
||||||
|
if !ok || subject.UserID <= 0 {
|
||||||
|
response.Error(c, http.StatusUnauthorized, "Unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := h.opsService.UpdateRuntimeLogConfig(c.Request.Context(), &req, subject.UserID)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetRuntimeLogConfig removes runtime override and falls back to env/yaml baseline.
|
||||||
|
// POST /api/v1/admin/ops/runtime/logging/reset
|
||||||
|
func (h *OpsHandler) ResetRuntimeLogConfig(c *gin.Context) {
|
||||||
|
if h.opsService == nil {
|
||||||
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subject, ok := middleware.GetAuthSubjectFromContext(c)
|
||||||
|
if !ok || subject.UserID <= 0 {
|
||||||
|
response.Error(c, http.StatusUnauthorized, "Unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := h.opsService.ResetRuntimeLogConfig(c.Request.Context(), subject.UserID)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, updated)
|
||||||
|
}
|
||||||
|
|
||||||
// GetAdvancedSettings returns Ops advanced settings (DB-backed).
|
// GetAdvancedSettings returns Ops advanced settings (DB-backed).
|
||||||
// GET /api/v1/admin/ops/advanced-settings
|
// GET /api/v1/admin/ops/advanced-settings
|
||||||
func (h *OpsHandler) GetAdvancedSettings(c *gin.Context) {
|
func (h *OpsHandler) GetAdvancedSettings(c *gin.Context) {
|
||||||
|
|||||||
174
backend/internal/handler/admin/ops_system_log_handler.go
Normal file
174
backend/internal/handler/admin/ops_system_log_handler.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type opsSystemLogCleanupRequest struct {
|
||||||
|
StartTime string `json:"start_time"`
|
||||||
|
EndTime string `json:"end_time"`
|
||||||
|
|
||||||
|
Level string `json:"level"`
|
||||||
|
Component string `json:"component"`
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
ClientRequestID string `json:"client_request_id"`
|
||||||
|
UserID *int64 `json:"user_id"`
|
||||||
|
AccountID *int64 `json:"account_id"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Query string `json:"q"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSystemLogs returns indexed system logs.
|
||||||
|
// GET /api/v1/admin/ops/system-logs
|
||||||
|
func (h *OpsHandler) ListSystemLogs(c *gin.Context) {
|
||||||
|
if h.opsService == nil {
|
||||||
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page, pageSize := response.ParsePagination(c)
|
||||||
|
if pageSize > 200 {
|
||||||
|
pageSize = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
start, end, err := parseOpsTimeRange(c, "1h")
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &service.OpsSystemLogFilter{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
StartTime: &start,
|
||||||
|
EndTime: &end,
|
||||||
|
Level: strings.TrimSpace(c.Query("level")),
|
||||||
|
Component: strings.TrimSpace(c.Query("component")),
|
||||||
|
RequestID: strings.TrimSpace(c.Query("request_id")),
|
||||||
|
ClientRequestID: strings.TrimSpace(c.Query("client_request_id")),
|
||||||
|
Platform: strings.TrimSpace(c.Query("platform")),
|
||||||
|
Model: strings.TrimSpace(c.Query("model")),
|
||||||
|
Query: strings.TrimSpace(c.Query("q")),
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(c.Query("user_id")); v != "" {
|
||||||
|
id, parseErr := strconv.ParseInt(v, 10, 64)
|
||||||
|
if parseErr != nil || id <= 0 {
|
||||||
|
response.BadRequest(c, "Invalid user_id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filter.UserID = &id
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(c.Query("account_id")); v != "" {
|
||||||
|
id, parseErr := strconv.ParseInt(v, 10, 64)
|
||||||
|
if parseErr != nil || id <= 0 {
|
||||||
|
response.BadRequest(c, "Invalid account_id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filter.AccountID = &id
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.opsService.ListSystemLogs(c.Request.Context(), filter)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Paginated(c, result.Logs, int64(result.Total), result.Page, result.PageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupSystemLogs deletes indexed system logs by filter.
|
||||||
|
// POST /api/v1/admin/ops/system-logs/cleanup
|
||||||
|
func (h *OpsHandler) CleanupSystemLogs(c *gin.Context) {
|
||||||
|
if h.opsService == nil {
|
||||||
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subject, ok := middleware.GetAuthSubjectFromContext(c)
|
||||||
|
if !ok || subject.UserID <= 0 {
|
||||||
|
response.Error(c, http.StatusUnauthorized, "Unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req opsSystemLogCleanupRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "Invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTS := func(raw string) (*time.Time, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if t, err := time.Parse(time.RFC3339Nano, raw); err == nil {
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
t, err := time.Parse(time.RFC3339, raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
start, err := parseTS(req.StartTime)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "Invalid start_time")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
end, err := parseTS(req.EndTime)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "Invalid end_time")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &service.OpsSystemLogCleanupFilter{
|
||||||
|
StartTime: start,
|
||||||
|
EndTime: end,
|
||||||
|
Level: strings.TrimSpace(req.Level),
|
||||||
|
Component: strings.TrimSpace(req.Component),
|
||||||
|
RequestID: strings.TrimSpace(req.RequestID),
|
||||||
|
ClientRequestID: strings.TrimSpace(req.ClientRequestID),
|
||||||
|
UserID: req.UserID,
|
||||||
|
AccountID: req.AccountID,
|
||||||
|
Platform: strings.TrimSpace(req.Platform),
|
||||||
|
Model: strings.TrimSpace(req.Model),
|
||||||
|
Query: strings.TrimSpace(req.Query),
|
||||||
|
}
|
||||||
|
|
||||||
|
deleted, err := h.opsService.CleanupSystemLogs(c.Request.Context(), filter, subject.UserID)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, gin.H{"deleted": deleted})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystemLogIngestionHealth returns sink health metrics.
|
||||||
|
// GET /api/v1/admin/ops/system-logs/health
|
||||||
|
func (h *OpsHandler) GetSystemLogIngestionHealth(c *gin.Context) {
|
||||||
|
if h.opsService == nil {
|
||||||
|
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.Success(c, h.opsService.GetSystemLogSinkHealth())
|
||||||
|
}
|
||||||
233
backend/internal/handler/admin/ops_system_log_handler_test.go
Normal file
233
backend/internal/handler/admin/ops_system_log_handler_test.go
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type responseEnvelope struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOpsSystemLogTestRouter(handler *OpsHandler, withUser bool) *gin.Engine {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.New()
|
||||||
|
if withUser {
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{UserID: 99})
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
r.GET("/logs", handler.ListSystemLogs)
|
||||||
|
r.POST("/logs/cleanup", handler.CleanupSystemLogs)
|
||||||
|
r.GET("/logs/health", handler.GetSystemLogIngestionHealth)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_ListUnavailable(t *testing.T) {
|
||||||
|
h := NewOpsHandler(nil)
|
||||||
|
r := newOpsSystemLogTestRouter(h, false)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/logs", nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusServiceUnavailable {
|
||||||
|
t.Fatalf("status=%d, want 503", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_ListInvalidUserID(t *testing.T) {
|
||||||
|
svc := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, false)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/logs?user_id=abc", nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status=%d, want 400", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_ListInvalidAccountID(t *testing.T) {
|
||||||
|
svc := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, false)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/logs?account_id=-1", nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status=%d, want 400", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_ListMonitoringDisabled(t *testing.T) {
|
||||||
|
svc := service.NewOpsService(nil, nil, &config.Config{
|
||||||
|
Ops: config.OpsConfig{Enabled: false},
|
||||||
|
}, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, false)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/logs", nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status=%d, want 404", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_ListSuccess(t *testing.T) {
|
||||||
|
svc := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, false)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/logs?time_range=30m&page=1&page_size=20", nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status=%d, want 200", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp responseEnvelope
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Code != 0 {
|
||||||
|
t.Fatalf("unexpected response code: %+v", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_CleanupUnauthorized(t *testing.T) {
|
||||||
|
svc := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, false)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/logs/cleanup", bytes.NewBufferString(`{"request_id":"r1"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("status=%d, want 401", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_CleanupInvalidPayload(t *testing.T) {
|
||||||
|
svc := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, true)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/logs/cleanup", bytes.NewBufferString(`{bad-json`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status=%d, want 400", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_CleanupInvalidTime(t *testing.T) {
|
||||||
|
svc := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, true)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/logs/cleanup", bytes.NewBufferString(`{"start_time":"bad","request_id":"r1"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status=%d, want 400", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_CleanupInvalidEndTime(t *testing.T) {
|
||||||
|
svc := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, true)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/logs/cleanup", bytes.NewBufferString(`{"end_time":"bad","request_id":"r1"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status=%d, want 400", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_CleanupServiceUnavailable(t *testing.T) {
|
||||||
|
svc := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, true)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/logs/cleanup", bytes.NewBufferString(`{"request_id":"r1"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusServiceUnavailable {
|
||||||
|
t.Fatalf("status=%d, want 503", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_CleanupMonitoringDisabled(t *testing.T) {
|
||||||
|
svc := service.NewOpsService(nil, nil, &config.Config{
|
||||||
|
Ops: config.OpsConfig{Enabled: false},
|
||||||
|
}, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, true)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/logs/cleanup", bytes.NewBufferString(`{"request_id":"r1"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status=%d, want 404", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_Health(t *testing.T) {
|
||||||
|
sink := service.NewOpsSystemLogSink(nil)
|
||||||
|
svc := service.NewOpsService(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, sink)
|
||||||
|
h := NewOpsHandler(svc)
|
||||||
|
r := newOpsSystemLogTestRouter(h, false)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/logs/health", nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status=%d, want 200", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpsSystemLogHandler_HealthUnavailableAndMonitoringDisabled(t *testing.T) {
|
||||||
|
h := NewOpsHandler(nil)
|
||||||
|
r := newOpsSystemLogTestRouter(h, false)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/logs/health", nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusServiceUnavailable {
|
||||||
|
t.Fatalf("status=%d, want 503", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc := service.NewOpsService(nil, nil, &config.Config{
|
||||||
|
Ops: config.OpsConfig{Enabled: false},
|
||||||
|
}, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
h = NewOpsHandler(svc)
|
||||||
|
r = newOpsSystemLogTestRouter(h, false)
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
req = httptest.NewRequest(http.MethodGet, "/logs/health", nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("status=%d, want 404", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -16,6 +15,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@@ -252,7 +252,7 @@ func (c *opsWSQPSCache) refresh(parentCtx context.Context) {
|
|||||||
stats, err := opsService.GetWindowStats(ctx, now.Add(-c.requestCountWindow), now)
|
stats, err := opsService.GetWindowStats(ctx, now.Add(-c.requestCountWindow), now)
|
||||||
if err != nil || stats == nil {
|
if err != nil || stats == nil {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[OpsWS] refresh: get window stats failed: %v", err)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] refresh: get window stats failed: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ func (c *opsWSQPSCache) refresh(parentCtx context.Context) {
|
|||||||
|
|
||||||
msg, err := json.Marshal(payload)
|
msg, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[OpsWS] refresh: marshal payload failed: %v", err)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] refresh: marshal payload failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,7 +338,7 @@ func (h *OpsHandler) QPSWSHandler(c *gin.Context) {
|
|||||||
|
|
||||||
// Reserve a global slot before upgrading the connection to keep the limit strict.
|
// Reserve a global slot before upgrading the connection to keep the limit strict.
|
||||||
if !tryAcquireOpsWSTotalSlot(opsWSLimits.MaxConns) {
|
if !tryAcquireOpsWSTotalSlot(opsWSLimits.MaxConns) {
|
||||||
log.Printf("[OpsWS] connection limit reached: %d/%d", wsConnCount.Load(), opsWSLimits.MaxConns)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] connection limit reached: %d/%d", wsConnCount.Load(), opsWSLimits.MaxConns)
|
||||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "too many connections"})
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "too many connections"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -350,7 +350,7 @@ func (h *OpsHandler) QPSWSHandler(c *gin.Context) {
|
|||||||
|
|
||||||
if opsWSLimits.MaxConnsPerIP > 0 && clientIP != "" {
|
if opsWSLimits.MaxConnsPerIP > 0 && clientIP != "" {
|
||||||
if !tryAcquireOpsWSIPSlot(clientIP, opsWSLimits.MaxConnsPerIP) {
|
if !tryAcquireOpsWSIPSlot(clientIP, opsWSLimits.MaxConnsPerIP) {
|
||||||
log.Printf("[OpsWS] per-ip connection limit reached: ip=%s limit=%d", clientIP, opsWSLimits.MaxConnsPerIP)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] per-ip connection limit reached: ip=%s limit=%d", clientIP, opsWSLimits.MaxConnsPerIP)
|
||||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "too many connections"})
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "too many connections"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -359,7 +359,7 @@ func (h *OpsHandler) QPSWSHandler(c *gin.Context) {
|
|||||||
|
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[OpsWS] upgrade failed: %v", err)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] upgrade failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,7 +452,7 @@ func handleQPSWebSocket(parentCtx context.Context, conn *websocket.Conn) {
|
|||||||
|
|
||||||
conn.SetReadLimit(qpsWSMaxReadBytes)
|
conn.SetReadLimit(qpsWSMaxReadBytes)
|
||||||
if err := conn.SetReadDeadline(time.Now().Add(qpsWSPongWait)); err != nil {
|
if err := conn.SetReadDeadline(time.Now().Add(qpsWSPongWait)); err != nil {
|
||||||
log.Printf("[OpsWS] set read deadline failed: %v", err)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] set read deadline failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conn.SetPongHandler(func(string) error {
|
conn.SetPongHandler(func(string) error {
|
||||||
@@ -471,7 +471,7 @@ func handleQPSWebSocket(parentCtx context.Context, conn *websocket.Conn) {
|
|||||||
_, _, err := conn.ReadMessage()
|
_, _, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) {
|
||||||
log.Printf("[OpsWS] read failed: %v", err)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] read failed: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -508,7 +508,7 @@ func handleQPSWebSocket(parentCtx context.Context, conn *websocket.Conn) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := writeWithTimeout(websocket.TextMessage, msg); err != nil {
|
if err := writeWithTimeout(websocket.TextMessage, msg); err != nil {
|
||||||
log.Printf("[OpsWS] write failed: %v", err)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] write failed: %v", err)
|
||||||
cancel()
|
cancel()
|
||||||
closeConn()
|
closeConn()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -517,7 +517,7 @@ func handleQPSWebSocket(parentCtx context.Context, conn *websocket.Conn) {
|
|||||||
|
|
||||||
case <-pingTicker.C:
|
case <-pingTicker.C:
|
||||||
if err := writeWithTimeout(websocket.PingMessage, nil); err != nil {
|
if err := writeWithTimeout(websocket.PingMessage, nil); err != nil {
|
||||||
log.Printf("[OpsWS] ping failed: %v", err)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] ping failed: %v", err)
|
||||||
cancel()
|
cancel()
|
||||||
closeConn()
|
closeConn()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -666,14 +666,14 @@ func loadOpsWSProxyConfigFromEnv() OpsWSProxyConfig {
|
|||||||
if parsed, err := strconv.ParseBool(v); err == nil {
|
if parsed, err := strconv.ParseBool(v); err == nil {
|
||||||
cfg.TrustProxy = parsed
|
cfg.TrustProxy = parsed
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[OpsWS] invalid %s=%q (expected bool); using default=%v", envOpsWSTrustProxy, v, cfg.TrustProxy)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] invalid %s=%q (expected bool); using default=%v", envOpsWSTrustProxy, v, cfg.TrustProxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if raw := strings.TrimSpace(os.Getenv(envOpsWSTrustedProxies)); raw != "" {
|
if raw := strings.TrimSpace(os.Getenv(envOpsWSTrustedProxies)); raw != "" {
|
||||||
prefixes, invalid := parseTrustedProxyList(raw)
|
prefixes, invalid := parseTrustedProxyList(raw)
|
||||||
if len(invalid) > 0 {
|
if len(invalid) > 0 {
|
||||||
log.Printf("[OpsWS] invalid %s entries ignored: %s", envOpsWSTrustedProxies, strings.Join(invalid, ", "))
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] invalid %s entries ignored: %s", envOpsWSTrustedProxies, strings.Join(invalid, ", "))
|
||||||
}
|
}
|
||||||
cfg.TrustedProxies = prefixes
|
cfg.TrustedProxies = prefixes
|
||||||
}
|
}
|
||||||
@@ -684,7 +684,7 @@ func loadOpsWSProxyConfigFromEnv() OpsWSProxyConfig {
|
|||||||
case OriginPolicyStrict, OriginPolicyPermissive:
|
case OriginPolicyStrict, OriginPolicyPermissive:
|
||||||
cfg.OriginPolicy = normalized
|
cfg.OriginPolicy = normalized
|
||||||
default:
|
default:
|
||||||
log.Printf("[OpsWS] invalid %s=%q (expected %q or %q); using default=%q", envOpsWSOriginPolicy, v, OriginPolicyStrict, OriginPolicyPermissive, cfg.OriginPolicy)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] invalid %s=%q (expected %q or %q); using default=%q", envOpsWSOriginPolicy, v, OriginPolicyStrict, OriginPolicyPermissive, cfg.OriginPolicy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,14 +701,14 @@ func loadOpsWSRuntimeLimitsFromEnv() opsWSRuntimeLimits {
|
|||||||
if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 {
|
if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 {
|
||||||
cfg.MaxConns = int32(parsed)
|
cfg.MaxConns = int32(parsed)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[OpsWS] invalid %s=%q (expected int>0); using default=%d", envOpsWSMaxConns, v, cfg.MaxConns)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] invalid %s=%q (expected int>0); using default=%d", envOpsWSMaxConns, v, cfg.MaxConns)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v := strings.TrimSpace(os.Getenv(envOpsWSMaxConnsPerIP)); v != "" {
|
if v := strings.TrimSpace(os.Getenv(envOpsWSMaxConnsPerIP)); v != "" {
|
||||||
if parsed, err := strconv.Atoi(v); err == nil && parsed >= 0 {
|
if parsed, err := strconv.Atoi(v); err == nil && parsed >= 0 {
|
||||||
cfg.MaxConnsPerIP = int32(parsed)
|
cfg.MaxConnsPerIP = int32(parsed)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[OpsWS] invalid %s=%q (expected int>=0); using default=%d", envOpsWSMaxConnsPerIP, v, cfg.MaxConnsPerIP)
|
logger.LegacyPrintf("handler.admin.ops_ws", "[OpsWS] invalid %s=%q (expected int>=0); using default=%d", envOpsWSMaxConnsPerIP, v, cfg.MaxConnsPerIP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cfg
|
return cfg
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -130,20 +131,20 @@ func (h *ProxyHandler) Create(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy, err := h.adminService.CreateProxy(c.Request.Context(), &service.CreateProxyInput{
|
executeAdminIdempotentJSON(c, "admin.proxies.create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
Name: strings.TrimSpace(req.Name),
|
proxy, err := h.adminService.CreateProxy(ctx, &service.CreateProxyInput{
|
||||||
Protocol: strings.TrimSpace(req.Protocol),
|
Name: strings.TrimSpace(req.Name),
|
||||||
Host: strings.TrimSpace(req.Host),
|
Protocol: strings.TrimSpace(req.Protocol),
|
||||||
Port: req.Port,
|
Host: strings.TrimSpace(req.Host),
|
||||||
Username: strings.TrimSpace(req.Username),
|
Port: req.Port,
|
||||||
Password: strings.TrimSpace(req.Password),
|
Username: strings.TrimSpace(req.Username),
|
||||||
|
Password: strings.TrimSpace(req.Password),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dto.ProxyFromService(proxy), nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
response.ErrorFrom(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Success(c, dto.ProxyFromService(proxy))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update handles updating a proxy
|
// Update handles updating a proxy
|
||||||
@@ -236,6 +237,24 @@ func (h *ProxyHandler) Test(c *gin.Context) {
|
|||||||
response.Success(c, result)
|
response.Success(c, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckQuality handles checking proxy quality across common AI targets.
|
||||||
|
// POST /api/v1/admin/proxies/:id/quality-check
|
||||||
|
func (h *ProxyHandler) CheckQuality(c *gin.Context) {
|
||||||
|
proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "Invalid proxy ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.adminService.CheckProxyQuality(c.Request.Context(), proxyID)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
// GetStats handles getting proxy statistics
|
// GetStats handles getting proxy statistics
|
||||||
// GET /api/v1/admin/proxies/:id/stats
|
// GET /api/v1/admin/proxies/:id/stats
|
||||||
func (h *ProxyHandler) GetStats(c *gin.Context) {
|
func (h *ProxyHandler) GetStats(c *gin.Context) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package admin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -88,23 +89,24 @@ func (h *RedeemHandler) Generate(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
codes, err := h.adminService.GenerateRedeemCodes(c.Request.Context(), &service.GenerateRedeemCodesInput{
|
executeAdminIdempotentJSON(c, "admin.redeem_codes.generate", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
Count: req.Count,
|
codes, execErr := h.adminService.GenerateRedeemCodes(ctx, &service.GenerateRedeemCodesInput{
|
||||||
Type: req.Type,
|
Count: req.Count,
|
||||||
Value: req.Value,
|
Type: req.Type,
|
||||||
GroupID: req.GroupID,
|
Value: req.Value,
|
||||||
ValidityDays: req.ValidityDays,
|
GroupID: req.GroupID,
|
||||||
})
|
ValidityDays: req.ValidityDays,
|
||||||
if err != nil {
|
})
|
||||||
response.ErrorFrom(c, err)
|
if execErr != nil {
|
||||||
return
|
return nil, execErr
|
||||||
}
|
}
|
||||||
|
|
||||||
out := make([]dto.AdminRedeemCode, 0, len(codes))
|
out := make([]dto.AdminRedeemCode, 0, len(codes))
|
||||||
for i := range codes {
|
for i := range codes {
|
||||||
out = append(out, *dto.RedeemCodeFromServiceAdmin(&codes[i]))
|
out = append(out, *dto.RedeemCodeFromServiceAdmin(&codes[i]))
|
||||||
}
|
}
|
||||||
response.Success(c, out)
|
return out, nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete handles deleting a redeem code
|
// Delete handles deleting a redeem code
|
||||||
@@ -202,7 +204,7 @@ func (h *RedeemHandler) Export(c *gin.Context) {
|
|||||||
writer := csv.NewWriter(&buf)
|
writer := csv.NewWriter(&buf)
|
||||||
|
|
||||||
// Write header
|
// Write header
|
||||||
if err := writer.Write([]string{"id", "code", "type", "value", "status", "used_by", "used_at", "created_at"}); err != nil {
|
if err := writer.Write([]string{"id", "code", "type", "value", "status", "used_by", "used_by_email", "used_at", "created_at"}); err != nil {
|
||||||
response.InternalError(c, "Failed to export redeem codes: "+err.Error())
|
response.InternalError(c, "Failed to export redeem codes: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -213,6 +215,10 @@ func (h *RedeemHandler) Export(c *gin.Context) {
|
|||||||
if code.UsedBy != nil {
|
if code.UsedBy != nil {
|
||||||
usedBy = fmt.Sprintf("%d", *code.UsedBy)
|
usedBy = fmt.Sprintf("%d", *code.UsedBy)
|
||||||
}
|
}
|
||||||
|
usedByEmail := ""
|
||||||
|
if code.User != nil {
|
||||||
|
usedByEmail = code.User.Email
|
||||||
|
}
|
||||||
usedAt := ""
|
usedAt := ""
|
||||||
if code.UsedAt != nil {
|
if code.UsedAt != nil {
|
||||||
usedAt = code.UsedAt.Format("2006-01-02 15:04:05")
|
usedAt = code.UsedAt.Format("2006-01-02 15:04:05")
|
||||||
@@ -224,6 +230,7 @@ func (h *RedeemHandler) Export(c *gin.Context) {
|
|||||||
fmt.Sprintf("%.2f", code.Value),
|
fmt.Sprintf("%.2f", code.Value),
|
||||||
code.Status,
|
code.Status,
|
||||||
usedBy,
|
usedBy,
|
||||||
|
usedByEmail,
|
||||||
usedAt,
|
usedAt,
|
||||||
code.CreatedAt.Format("2006-01-02 15:04:05"),
|
code.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|||||||
97
backend/internal/handler/admin/search_truncate_test.go
Normal file
97
backend/internal/handler/admin/search_truncate_test.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
//go:build unit
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// truncateSearchByRune 模拟 user_handler.go 中的 search 截断逻辑
|
||||||
|
func truncateSearchByRune(search string, maxRunes int) string {
|
||||||
|
if runes := []rune(search); len(runes) > maxRunes {
|
||||||
|
return string(runes[:maxRunes])
|
||||||
|
}
|
||||||
|
return search
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncateSearchByRune(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
maxRunes int
|
||||||
|
wantLen int // 期望的 rune 长度
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "纯中文超长",
|
||||||
|
input: string(make([]rune, 150)),
|
||||||
|
maxRunes: 100,
|
||||||
|
wantLen: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "纯 ASCII 超长",
|
||||||
|
input: string(make([]byte, 150)),
|
||||||
|
maxRunes: 100,
|
||||||
|
wantLen: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "空字符串",
|
||||||
|
input: "",
|
||||||
|
maxRunes: 100,
|
||||||
|
wantLen: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "恰好 100 个字符",
|
||||||
|
input: string(make([]rune, 100)),
|
||||||
|
maxRunes: 100,
|
||||||
|
wantLen: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "不足 100 字符不截断",
|
||||||
|
input: "hello世界",
|
||||||
|
maxRunes: 100,
|
||||||
|
wantLen: 7,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := truncateSearchByRune(tc.input, tc.maxRunes)
|
||||||
|
require.Equal(t, tc.wantLen, len([]rune(result)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncateSearchByRune_PreservesMultibyte(t *testing.T) {
|
||||||
|
// 101 个中文字符,截断到 100 个后应该仍然是有效 UTF-8
|
||||||
|
input := ""
|
||||||
|
for i := 0; i < 101; i++ {
|
||||||
|
input += "中"
|
||||||
|
}
|
||||||
|
result := truncateSearchByRune(input, 100)
|
||||||
|
|
||||||
|
require.Equal(t, 100, len([]rune(result)))
|
||||||
|
// 验证截断结果是有效的 UTF-8(每个中文字符 3 字节)
|
||||||
|
require.Equal(t, 300, len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncateSearchByRune_MixedASCIIAndMultibyte(t *testing.T) {
|
||||||
|
// 50 个 ASCII + 51 个中文 = 101 个 rune
|
||||||
|
input := ""
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
input += "a"
|
||||||
|
}
|
||||||
|
for i := 0; i < 51; i++ {
|
||||||
|
input += "中"
|
||||||
|
}
|
||||||
|
result := truncateSearchByRune(input, 100)
|
||||||
|
|
||||||
|
runes := []rune(result)
|
||||||
|
require.Equal(t, 100, len(runes))
|
||||||
|
// 前 50 个应该是 'a',后 50 个应该是 '中'
|
||||||
|
require.Equal(t, 'a', runes[0])
|
||||||
|
require.Equal(t, 'a', runes[49])
|
||||||
|
require.Equal(t, '中', runes[50])
|
||||||
|
require.Equal(t, '中', runes[99])
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||||
@@ -199,13 +200,20 @@ func (h *SubscriptionHandler) Extend(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription, err := h.subscriptionService.ExtendSubscription(c.Request.Context(), subscriptionID, req.Days)
|
idempotencyPayload := struct {
|
||||||
if err != nil {
|
SubscriptionID int64 `json:"subscription_id"`
|
||||||
response.ErrorFrom(c, err)
|
Body AdjustSubscriptionRequest `json:"body"`
|
||||||
return
|
}{
|
||||||
|
SubscriptionID: subscriptionID,
|
||||||
|
Body: req,
|
||||||
}
|
}
|
||||||
|
executeAdminIdempotentJSON(c, "admin.subscriptions.extend", idempotencyPayload, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
|
subscription, execErr := h.subscriptionService.ExtendSubscription(ctx, subscriptionID, req.Days)
|
||||||
|
if execErr != nil {
|
||||||
|
return nil, execErr
|
||||||
|
}
|
||||||
|
return dto.UserSubscriptionFromServiceAdmin(subscription), nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revoke handles revoking a subscription
|
// Revoke handles revoking a subscription
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/sysutil"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/sysutil"
|
||||||
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -14,12 +18,14 @@ import (
|
|||||||
// SystemHandler handles system-related operations
|
// SystemHandler handles system-related operations
|
||||||
type SystemHandler struct {
|
type SystemHandler struct {
|
||||||
updateSvc *service.UpdateService
|
updateSvc *service.UpdateService
|
||||||
|
lockSvc *service.SystemOperationLockService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSystemHandler creates a new SystemHandler
|
// NewSystemHandler creates a new SystemHandler
|
||||||
func NewSystemHandler(updateSvc *service.UpdateService) *SystemHandler {
|
func NewSystemHandler(updateSvc *service.UpdateService, lockSvc *service.SystemOperationLockService) *SystemHandler {
|
||||||
return &SystemHandler{
|
return &SystemHandler{
|
||||||
updateSvc: updateSvc,
|
updateSvc: updateSvc,
|
||||||
|
lockSvc: lockSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,41 +53,125 @@ func (h *SystemHandler) CheckUpdates(c *gin.Context) {
|
|||||||
// PerformUpdate downloads and applies the update
|
// PerformUpdate downloads and applies the update
|
||||||
// POST /api/v1/admin/system/update
|
// POST /api/v1/admin/system/update
|
||||||
func (h *SystemHandler) PerformUpdate(c *gin.Context) {
|
func (h *SystemHandler) PerformUpdate(c *gin.Context) {
|
||||||
if err := h.updateSvc.PerformUpdate(c.Request.Context()); err != nil {
|
operationID := buildSystemOperationID(c, "update")
|
||||||
response.Error(c, http.StatusInternalServerError, err.Error())
|
payload := gin.H{"operation_id": operationID}
|
||||||
return
|
executeAdminIdempotentJSON(c, "admin.system.update", payload, service.DefaultSystemOperationIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
}
|
lock, release, err := h.acquireSystemLock(ctx, operationID)
|
||||||
response.Success(c, gin.H{
|
if err != nil {
|
||||||
"message": "Update completed. Please restart the service.",
|
return nil, err
|
||||||
"need_restart": true,
|
}
|
||||||
|
var releaseReason string
|
||||||
|
succeeded := false
|
||||||
|
defer func() {
|
||||||
|
release(releaseReason, succeeded)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := h.updateSvc.PerformUpdate(ctx); err != nil {
|
||||||
|
releaseReason = "SYSTEM_UPDATE_FAILED"
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
succeeded = true
|
||||||
|
|
||||||
|
return gin.H{
|
||||||
|
"message": "Update completed. Please restart the service.",
|
||||||
|
"need_restart": true,
|
||||||
|
"operation_id": lock.OperationID(),
|
||||||
|
}, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rollback restores the previous version
|
// Rollback restores the previous version
|
||||||
// POST /api/v1/admin/system/rollback
|
// POST /api/v1/admin/system/rollback
|
||||||
func (h *SystemHandler) Rollback(c *gin.Context) {
|
func (h *SystemHandler) Rollback(c *gin.Context) {
|
||||||
if err := h.updateSvc.Rollback(); err != nil {
|
operationID := buildSystemOperationID(c, "rollback")
|
||||||
response.Error(c, http.StatusInternalServerError, err.Error())
|
payload := gin.H{"operation_id": operationID}
|
||||||
return
|
executeAdminIdempotentJSON(c, "admin.system.rollback", payload, service.DefaultSystemOperationIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
}
|
lock, release, err := h.acquireSystemLock(ctx, operationID)
|
||||||
response.Success(c, gin.H{
|
if err != nil {
|
||||||
"message": "Rollback completed. Please restart the service.",
|
return nil, err
|
||||||
"need_restart": true,
|
}
|
||||||
|
var releaseReason string
|
||||||
|
succeeded := false
|
||||||
|
defer func() {
|
||||||
|
release(releaseReason, succeeded)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := h.updateSvc.Rollback(); err != nil {
|
||||||
|
releaseReason = "SYSTEM_ROLLBACK_FAILED"
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
succeeded = true
|
||||||
|
|
||||||
|
return gin.H{
|
||||||
|
"message": "Rollback completed. Please restart the service.",
|
||||||
|
"need_restart": true,
|
||||||
|
"operation_id": lock.OperationID(),
|
||||||
|
}, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestartService restarts the systemd service
|
// RestartService restarts the systemd service
|
||||||
// POST /api/v1/admin/system/restart
|
// POST /api/v1/admin/system/restart
|
||||||
func (h *SystemHandler) RestartService(c *gin.Context) {
|
func (h *SystemHandler) RestartService(c *gin.Context) {
|
||||||
// Schedule service restart in background after sending response
|
operationID := buildSystemOperationID(c, "restart")
|
||||||
// This ensures the client receives the success response before the service restarts
|
payload := gin.H{"operation_id": operationID}
|
||||||
go func() {
|
executeAdminIdempotentJSON(c, "admin.system.restart", payload, service.DefaultSystemOperationIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
// Wait a moment to ensure the response is sent
|
lock, release, err := h.acquireSystemLock(ctx, operationID)
|
||||||
time.Sleep(500 * time.Millisecond)
|
if err != nil {
|
||||||
sysutil.RestartServiceAsync()
|
return nil, err
|
||||||
}()
|
}
|
||||||
|
succeeded := false
|
||||||
|
defer func() {
|
||||||
|
release("", succeeded)
|
||||||
|
}()
|
||||||
|
|
||||||
response.Success(c, gin.H{
|
// Schedule service restart in background after sending response
|
||||||
"message": "Service restart initiated",
|
// This ensures the client receives the success response before the service restarts
|
||||||
|
go func() {
|
||||||
|
// Wait a moment to ensure the response is sent
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
sysutil.RestartServiceAsync()
|
||||||
|
}()
|
||||||
|
succeeded = true
|
||||||
|
return gin.H{
|
||||||
|
"message": "Service restart initiated",
|
||||||
|
"operation_id": lock.OperationID(),
|
||||||
|
}, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *SystemHandler) acquireSystemLock(
|
||||||
|
ctx context.Context,
|
||||||
|
operationID string,
|
||||||
|
) (*service.SystemOperationLock, func(string, bool), error) {
|
||||||
|
if h.lockSvc == nil {
|
||||||
|
return nil, nil, service.ErrIdempotencyStoreUnavail
|
||||||
|
}
|
||||||
|
lock, err := h.lockSvc.Acquire(ctx, operationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
release := func(reason string, succeeded bool) {
|
||||||
|
releaseCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = h.lockSvc.Release(releaseCtx, lock, succeeded, reason)
|
||||||
|
}
|
||||||
|
return lock, release, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSystemOperationID(c *gin.Context, operation string) string {
|
||||||
|
key := strings.TrimSpace(c.GetHeader("Idempotency-Key"))
|
||||||
|
if key == "" {
|
||||||
|
return "sysop-" + operation + "-" + strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||||
|
}
|
||||||
|
actorScope := "admin:0"
|
||||||
|
if subject, ok := middleware2.GetAuthSubjectFromContext(c); ok {
|
||||||
|
actorScope = "admin:" + strconv.FormatInt(subject.UserID, 10)
|
||||||
|
}
|
||||||
|
seed := operation + "|" + actorScope + "|" + c.FullPath() + "|" + key
|
||||||
|
hash := service.HashIdempotencyKey(seed)
|
||||||
|
if len(hash) > 24 {
|
||||||
|
hash = hash[:24]
|
||||||
|
}
|
||||||
|
return "sysop-" + hash
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
|
||||||
@@ -378,11 +379,11 @@ func (h *UsageHandler) ListCleanupTasks(c *gin.Context) {
|
|||||||
operator = subject.UserID
|
operator = subject.UserID
|
||||||
}
|
}
|
||||||
page, pageSize := response.ParsePagination(c)
|
page, pageSize := response.ParsePagination(c)
|
||||||
log.Printf("[UsageCleanup] 请求清理任务列表: operator=%d page=%d page_size=%d", operator, page, pageSize)
|
logger.LegacyPrintf("handler.admin.usage", "[UsageCleanup] 请求清理任务列表: operator=%d page=%d page_size=%d", operator, page, pageSize)
|
||||||
params := pagination.PaginationParams{Page: page, PageSize: pageSize}
|
params := pagination.PaginationParams{Page: page, PageSize: pageSize}
|
||||||
tasks, result, err := h.cleanupService.ListTasks(c.Request.Context(), params)
|
tasks, result, err := h.cleanupService.ListTasks(c.Request.Context(), params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[UsageCleanup] 查询清理任务列表失败: operator=%d page=%d page_size=%d err=%v", operator, page, pageSize, err)
|
logger.LegacyPrintf("handler.admin.usage", "[UsageCleanup] 查询清理任务列表失败: operator=%d page=%d page_size=%d err=%v", operator, page, pageSize, err)
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -390,7 +391,7 @@ func (h *UsageHandler) ListCleanupTasks(c *gin.Context) {
|
|||||||
for i := range tasks {
|
for i := range tasks {
|
||||||
out = append(out, *dto.UsageCleanupTaskFromService(&tasks[i]))
|
out = append(out, *dto.UsageCleanupTaskFromService(&tasks[i]))
|
||||||
}
|
}
|
||||||
log.Printf("[UsageCleanup] 返回清理任务列表: operator=%d total=%d items=%d page=%d page_size=%d", operator, result.Total, len(out), page, pageSize)
|
logger.LegacyPrintf("handler.admin.usage", "[UsageCleanup] 返回清理任务列表: operator=%d total=%d items=%d page=%d page_size=%d", operator, result.Total, len(out), page, pageSize)
|
||||||
response.Paginated(c, out, result.Total, page, pageSize)
|
response.Paginated(c, out, result.Total, page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,29 +473,36 @@ func (h *UsageHandler) CreateCleanupTask(c *gin.Context) {
|
|||||||
billingType = *filters.BillingType
|
billingType = *filters.BillingType
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[UsageCleanup] 请求创建清理任务: operator=%d start=%s end=%s user_id=%v api_key_id=%v account_id=%v group_id=%v model=%v stream=%v billing_type=%v tz=%q",
|
idempotencyPayload := struct {
|
||||||
subject.UserID,
|
OperatorID int64 `json:"operator_id"`
|
||||||
filters.StartTime.Format(time.RFC3339),
|
Body CreateUsageCleanupTaskRequest `json:"body"`
|
||||||
filters.EndTime.Format(time.RFC3339),
|
}{
|
||||||
userID,
|
OperatorID: subject.UserID,
|
||||||
apiKeyID,
|
Body: req,
|
||||||
accountID,
|
|
||||||
groupID,
|
|
||||||
model,
|
|
||||||
stream,
|
|
||||||
billingType,
|
|
||||||
req.Timezone,
|
|
||||||
)
|
|
||||||
|
|
||||||
task, err := h.cleanupService.CreateTask(c.Request.Context(), filters, subject.UserID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[UsageCleanup] 创建清理任务失败: operator=%d err=%v", subject.UserID, err)
|
|
||||||
response.ErrorFrom(c, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
executeAdminIdempotentJSON(c, "admin.usage.cleanup_tasks.create", idempotencyPayload, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
|
logger.LegacyPrintf("handler.admin.usage", "[UsageCleanup] 请求创建清理任务: operator=%d start=%s end=%s user_id=%v api_key_id=%v account_id=%v group_id=%v model=%v stream=%v billing_type=%v tz=%q",
|
||||||
|
subject.UserID,
|
||||||
|
filters.StartTime.Format(time.RFC3339),
|
||||||
|
filters.EndTime.Format(time.RFC3339),
|
||||||
|
userID,
|
||||||
|
apiKeyID,
|
||||||
|
accountID,
|
||||||
|
groupID,
|
||||||
|
model,
|
||||||
|
stream,
|
||||||
|
billingType,
|
||||||
|
req.Timezone,
|
||||||
|
)
|
||||||
|
|
||||||
log.Printf("[UsageCleanup] 清理任务已创建: task=%d operator=%d status=%s", task.ID, subject.UserID, task.Status)
|
task, err := h.cleanupService.CreateTask(ctx, filters, subject.UserID)
|
||||||
response.Success(c, dto.UsageCleanupTaskFromService(task))
|
if err != nil {
|
||||||
|
logger.LegacyPrintf("handler.admin.usage", "[UsageCleanup] 创建清理任务失败: operator=%d err=%v", subject.UserID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logger.LegacyPrintf("handler.admin.usage", "[UsageCleanup] 清理任务已创建: task=%d operator=%d status=%s", task.ID, subject.UserID, task.Status)
|
||||||
|
return dto.UsageCleanupTaskFromService(task), nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelCleanupTask handles canceling a usage cleanup task
|
// CancelCleanupTask handles canceling a usage cleanup task
|
||||||
@@ -515,12 +523,12 @@ func (h *UsageHandler) CancelCleanupTask(c *gin.Context) {
|
|||||||
response.BadRequest(c, "Invalid task id")
|
response.BadRequest(c, "Invalid task id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("[UsageCleanup] 请求取消清理任务: task=%d operator=%d", taskID, subject.UserID)
|
logger.LegacyPrintf("handler.admin.usage", "[UsageCleanup] 请求取消清理任务: task=%d operator=%d", taskID, subject.UserID)
|
||||||
if err := h.cleanupService.CancelTask(c.Request.Context(), taskID, subject.UserID); err != nil {
|
if err := h.cleanupService.CancelTask(c.Request.Context(), taskID, subject.UserID); err != nil {
|
||||||
log.Printf("[UsageCleanup] 取消清理任务失败: task=%d operator=%d err=%v", taskID, subject.UserID, err)
|
logger.LegacyPrintf("handler.admin.usage", "[UsageCleanup] 取消清理任务失败: task=%d operator=%d err=%v", taskID, subject.UserID, err)
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("[UsageCleanup] 清理任务已取消: task=%d operator=%d", taskID, subject.UserID)
|
logger.LegacyPrintf("handler.admin.usage", "[UsageCleanup] 清理任务已取消: task=%d operator=%d", taskID, subject.UserID)
|
||||||
response.Success(c, gin.H{"id": taskID, "status": service.UsageCleanupStatusCanceled})
|
response.Success(c, gin.H{"id": taskID, "status": service.UsageCleanupStatusCanceled})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -78,8 +79,8 @@ func (h *UserHandler) List(c *gin.Context) {
|
|||||||
search := c.Query("search")
|
search := c.Query("search")
|
||||||
// 标准化和验证 search 参数
|
// 标准化和验证 search 参数
|
||||||
search = strings.TrimSpace(search)
|
search = strings.TrimSpace(search)
|
||||||
if len(search) > 100 {
|
if runes := []rune(search); len(runes) > 100 {
|
||||||
search = search[:100]
|
search = string(runes[:100])
|
||||||
}
|
}
|
||||||
|
|
||||||
filters := service.UserListFilters{
|
filters := service.UserListFilters{
|
||||||
@@ -257,13 +258,20 @@ func (h *UserHandler) UpdateBalance(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.adminService.UpdateUserBalance(c.Request.Context(), userID, req.Balance, req.Operation, req.Notes)
|
idempotencyPayload := struct {
|
||||||
if err != nil {
|
UserID int64 `json:"user_id"`
|
||||||
response.ErrorFrom(c, err)
|
Body UpdateBalanceRequest `json:"body"`
|
||||||
return
|
}{
|
||||||
|
UserID: userID,
|
||||||
|
Body: req,
|
||||||
}
|
}
|
||||||
|
executeAdminIdempotentJSON(c, "admin.users.balance.update", idempotencyPayload, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
response.Success(c, dto.UserFromServiceAdmin(user))
|
user, execErr := h.adminService.UpdateUserBalance(ctx, userID, req.Balance, req.Operation, req.Notes)
|
||||||
|
if execErr != nil {
|
||||||
|
return nil, execErr
|
||||||
|
}
|
||||||
|
return dto.UserFromServiceAdmin(user), nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserAPIKeys handles getting user's API keys
|
// GetUserAPIKeys handles getting user's API keys
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -130,13 +131,14 @@ func (h *APIKeyHandler) Create(c *gin.Context) {
|
|||||||
if req.Quota != nil {
|
if req.Quota != nil {
|
||||||
svcReq.Quota = *req.Quota
|
svcReq.Quota = *req.Quota
|
||||||
}
|
}
|
||||||
key, err := h.apiKeyService.Create(c.Request.Context(), subject.UserID, svcReq)
|
|
||||||
if err != nil {
|
|
||||||
response.ErrorFrom(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Success(c, dto.APIKeyFromService(key))
|
executeUserIdempotentJSON(c, "user.api_keys.create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) {
|
||||||
|
key, err := h.apiKeyService.Create(ctx, subject.UserID, svcReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dto.APIKeyFromService(key), nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update handles updating an API key
|
// Update handles updating an API key
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
|
||||||
@@ -112,12 +113,11 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turnstile 验证(当提供了邮箱验证码时跳过,因为发送验证码时已验证过)
|
// Turnstile 验证 — 始终执行,防止绕过
|
||||||
if req.VerifyCode == "" {
|
// TODO: 确认前端在提交邮箱验证码注册时也传递了 turnstile_token
|
||||||
if err := h.authService.VerifyTurnstile(c.Request.Context(), req.TurnstileToken, ip.GetClientIP(c)); err != nil {
|
if err := h.authService.VerifyTurnstile(c.Request.Context(), req.TurnstileToken, ip.GetClientIP(c)); err != nil {
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, user, err := h.authService.RegisterWithVerification(c.Request.Context(), req.Email, req.Password, req.VerifyCode, req.PromoCode, req.InvitationCode)
|
_, user, err := h.authService.RegisterWithVerification(c.Request.Context(), req.Email, req.Password, req.VerifyCode, req.PromoCode, req.InvitationCode)
|
||||||
@@ -448,17 +448,12 @@ func (h *AuthHandler) ForgotPassword(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build frontend base URL from request
|
frontendBaseURL := strings.TrimSpace(h.cfg.Server.FrontendURL)
|
||||||
scheme := "https"
|
if frontendBaseURL == "" {
|
||||||
if c.Request.TLS == nil {
|
slog.Error("server.frontend_url not configured; cannot build password reset link")
|
||||||
// Check X-Forwarded-Proto header (common in reverse proxy setups)
|
response.InternalError(c, "Password reset is not configured")
|
||||||
if proto := c.GetHeader("X-Forwarded-Proto"); proto != "" {
|
return
|
||||||
scheme = proto
|
|
||||||
} else {
|
|
||||||
scheme = "http"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
frontendBaseURL := scheme + "://" + c.Request.Host
|
|
||||||
|
|
||||||
// Request password reset (async)
|
// Request password reset (async)
|
||||||
// Note: This returns success even if email doesn't exist (to prevent enumeration)
|
// Note: This returns success even if email doesn't exist (to prevent enumeration)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPIKeyFromService_MapsLastUsedAt(t *testing.T) {
|
||||||
|
lastUsed := time.Now().UTC().Truncate(time.Second)
|
||||||
|
src := &service.APIKey{
|
||||||
|
ID: 1,
|
||||||
|
UserID: 2,
|
||||||
|
Key: "sk-map-last-used",
|
||||||
|
Name: "Mapper",
|
||||||
|
Status: service.StatusActive,
|
||||||
|
LastUsedAt: &lastUsed,
|
||||||
|
}
|
||||||
|
|
||||||
|
out := APIKeyFromService(src)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
require.NotNil(t, out.LastUsedAt)
|
||||||
|
require.WithinDuration(t, lastUsed, *out.LastUsedAt, time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyFromService_MapsNilLastUsedAt(t *testing.T) {
|
||||||
|
src := &service.APIKey{
|
||||||
|
ID: 1,
|
||||||
|
UserID: 2,
|
||||||
|
Key: "sk-map-last-used-nil",
|
||||||
|
Name: "MapperNil",
|
||||||
|
Status: service.StatusActive,
|
||||||
|
}
|
||||||
|
|
||||||
|
out := APIKeyFromService(src)
|
||||||
|
require.NotNil(t, out)
|
||||||
|
require.Nil(t, out.LastUsedAt)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
@@ -77,6 +78,7 @@ func APIKeyFromService(k *service.APIKey) *APIKey {
|
|||||||
Status: k.Status,
|
Status: k.Status,
|
||||||
IPWhitelist: k.IPWhitelist,
|
IPWhitelist: k.IPWhitelist,
|
||||||
IPBlacklist: k.IPBlacklist,
|
IPBlacklist: k.IPBlacklist,
|
||||||
|
LastUsedAt: k.LastUsedAt,
|
||||||
Quota: k.Quota,
|
Quota: k.Quota,
|
||||||
QuotaUsed: k.QuotaUsed,
|
QuotaUsed: k.QuotaUsed,
|
||||||
ExpiresAt: k.ExpiresAt,
|
ExpiresAt: k.ExpiresAt,
|
||||||
@@ -129,23 +131,26 @@ func GroupFromServiceAdmin(g *service.Group) *AdminGroup {
|
|||||||
|
|
||||||
func groupFromServiceBase(g *service.Group) Group {
|
func groupFromServiceBase(g *service.Group) Group {
|
||||||
return Group{
|
return Group{
|
||||||
ID: g.ID,
|
ID: g.ID,
|
||||||
Name: g.Name,
|
Name: g.Name,
|
||||||
Description: g.Description,
|
Description: g.Description,
|
||||||
Platform: g.Platform,
|
Platform: g.Platform,
|
||||||
RateMultiplier: g.RateMultiplier,
|
RateMultiplier: g.RateMultiplier,
|
||||||
IsExclusive: g.IsExclusive,
|
IsExclusive: g.IsExclusive,
|
||||||
Status: g.Status,
|
Status: g.Status,
|
||||||
SubscriptionType: g.SubscriptionType,
|
SubscriptionType: g.SubscriptionType,
|
||||||
DailyLimitUSD: g.DailyLimitUSD,
|
DailyLimitUSD: g.DailyLimitUSD,
|
||||||
WeeklyLimitUSD: g.WeeklyLimitUSD,
|
WeeklyLimitUSD: g.WeeklyLimitUSD,
|
||||||
MonthlyLimitUSD: g.MonthlyLimitUSD,
|
MonthlyLimitUSD: g.MonthlyLimitUSD,
|
||||||
ImagePrice1K: g.ImagePrice1K,
|
ImagePrice1K: g.ImagePrice1K,
|
||||||
ImagePrice2K: g.ImagePrice2K,
|
ImagePrice2K: g.ImagePrice2K,
|
||||||
ImagePrice4K: g.ImagePrice4K,
|
ImagePrice4K: g.ImagePrice4K,
|
||||||
ClaudeCodeOnly: g.ClaudeCodeOnly,
|
SoraImagePrice360: g.SoraImagePrice360,
|
||||||
FallbackGroupID: g.FallbackGroupID,
|
SoraImagePrice540: g.SoraImagePrice540,
|
||||||
// 无效请求兜底分组
|
SoraVideoPricePerRequest: g.SoraVideoPricePerRequest,
|
||||||
|
SoraVideoPricePerRequestHD: g.SoraVideoPricePerRequestHD,
|
||||||
|
ClaudeCodeOnly: g.ClaudeCodeOnly,
|
||||||
|
FallbackGroupID: g.FallbackGroupID,
|
||||||
FallbackGroupIDOnInvalidRequest: g.FallbackGroupIDOnInvalidRequest,
|
FallbackGroupIDOnInvalidRequest: g.FallbackGroupIDOnInvalidRequest,
|
||||||
CreatedAt: g.CreatedAt,
|
CreatedAt: g.CreatedAt,
|
||||||
UpdatedAt: g.UpdatedAt,
|
UpdatedAt: g.UpdatedAt,
|
||||||
@@ -211,6 +216,13 @@ func AccountFromServiceShallow(a *service.Account) *Account {
|
|||||||
enabled := true
|
enabled := true
|
||||||
out.EnableSessionIDMasking = &enabled
|
out.EnableSessionIDMasking = &enabled
|
||||||
}
|
}
|
||||||
|
// 缓存 TTL 强制替换
|
||||||
|
if a.IsCacheTTLOverrideEnabled() {
|
||||||
|
enabled := true
|
||||||
|
out.CacheTTLOverrideEnabled = &enabled
|
||||||
|
target := a.GetCacheTTLOverrideTarget()
|
||||||
|
out.CacheTTLOverrideTarget = &target
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
@@ -293,6 +305,11 @@ func ProxyWithAccountCountFromService(p *service.ProxyWithAccountCount) *ProxyWi
|
|||||||
CountryCode: p.CountryCode,
|
CountryCode: p.CountryCode,
|
||||||
Region: p.Region,
|
Region: p.Region,
|
||||||
City: p.City,
|
City: p.City,
|
||||||
|
QualityStatus: p.QualityStatus,
|
||||||
|
QualityScore: p.QualityScore,
|
||||||
|
QualityGrade: p.QualityGrade,
|
||||||
|
QualitySummary: p.QualitySummary,
|
||||||
|
QualityChecked: p.QualityChecked,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +414,9 @@ func usageLogFromServiceUser(l *service.UsageLog) UsageLog {
|
|||||||
FirstTokenMs: l.FirstTokenMs,
|
FirstTokenMs: l.FirstTokenMs,
|
||||||
ImageCount: l.ImageCount,
|
ImageCount: l.ImageCount,
|
||||||
ImageSize: l.ImageSize,
|
ImageSize: l.ImageSize,
|
||||||
|
MediaType: l.MediaType,
|
||||||
UserAgent: l.UserAgent,
|
UserAgent: l.UserAgent,
|
||||||
|
CacheTTLOverridden: l.CacheTTLOverridden,
|
||||||
CreatedAt: l.CreatedAt,
|
CreatedAt: l.CreatedAt,
|
||||||
User: UserFromServiceShallow(l.User),
|
User: UserFromServiceShallow(l.User),
|
||||||
APIKey: APIKeyFromService(l.APIKey),
|
APIKey: APIKeyFromService(l.APIKey),
|
||||||
@@ -524,11 +543,18 @@ func BulkAssignResultFromService(r *service.BulkAssignResult) *BulkAssignResult
|
|||||||
for i := range r.Subscriptions {
|
for i := range r.Subscriptions {
|
||||||
subs = append(subs, *UserSubscriptionFromServiceAdmin(&r.Subscriptions[i]))
|
subs = append(subs, *UserSubscriptionFromServiceAdmin(&r.Subscriptions[i]))
|
||||||
}
|
}
|
||||||
|
statuses := make(map[string]string, len(r.Statuses))
|
||||||
|
for userID, status := range r.Statuses {
|
||||||
|
statuses[strconv.FormatInt(userID, 10)] = status
|
||||||
|
}
|
||||||
return &BulkAssignResult{
|
return &BulkAssignResult{
|
||||||
SuccessCount: r.SuccessCount,
|
SuccessCount: r.SuccessCount,
|
||||||
|
CreatedCount: r.CreatedCount,
|
||||||
|
ReusedCount: r.ReusedCount,
|
||||||
FailedCount: r.FailedCount,
|
FailedCount: r.FailedCount,
|
||||||
Subscriptions: subs,
|
Subscriptions: subs,
|
||||||
Errors: r.Errors,
|
Errors: r.Errors,
|
||||||
|
Statuses: statuses,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type APIKey struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
IPWhitelist []string `json:"ip_whitelist"`
|
IPWhitelist []string `json:"ip_whitelist"`
|
||||||
IPBlacklist []string `json:"ip_blacklist"`
|
IPBlacklist []string `json:"ip_blacklist"`
|
||||||
|
LastUsedAt *time.Time `json:"last_used_at"`
|
||||||
Quota float64 `json:"quota"` // Quota limit in USD (0 = unlimited)
|
Quota float64 `json:"quota"` // Quota limit in USD (0 = unlimited)
|
||||||
QuotaUsed float64 `json:"quota_used"` // Used quota amount in USD
|
QuotaUsed float64 `json:"quota_used"` // Used quota amount in USD
|
||||||
ExpiresAt *time.Time `json:"expires_at"` // Expiration time (nil = never expires)
|
ExpiresAt *time.Time `json:"expires_at"` // Expiration time (nil = never expires)
|
||||||
@@ -67,6 +68,12 @@ type Group struct {
|
|||||||
ImagePrice2K *float64 `json:"image_price_2k"`
|
ImagePrice2K *float64 `json:"image_price_2k"`
|
||||||
ImagePrice4K *float64 `json:"image_price_4k"`
|
ImagePrice4K *float64 `json:"image_price_4k"`
|
||||||
|
|
||||||
|
// Sora 按次计费配置
|
||||||
|
SoraImagePrice360 *float64 `json:"sora_image_price_360"`
|
||||||
|
SoraImagePrice540 *float64 `json:"sora_image_price_540"`
|
||||||
|
SoraVideoPricePerRequest *float64 `json:"sora_video_price_per_request"`
|
||||||
|
SoraVideoPricePerRequestHD *float64 `json:"sora_video_price_per_request_hd"`
|
||||||
|
|
||||||
// Claude Code 客户端限制
|
// Claude Code 客户端限制
|
||||||
ClaudeCodeOnly bool `json:"claude_code_only"`
|
ClaudeCodeOnly bool `json:"claude_code_only"`
|
||||||
FallbackGroupID *int64 `json:"fallback_group_id"`
|
FallbackGroupID *int64 `json:"fallback_group_id"`
|
||||||
@@ -150,6 +157,11 @@ type Account struct {
|
|||||||
// 从 extra 字段提取,方便前端显示和编辑
|
// 从 extra 字段提取,方便前端显示和编辑
|
||||||
EnableSessionIDMasking *bool `json:"session_id_masking_enabled,omitempty"`
|
EnableSessionIDMasking *bool `json:"session_id_masking_enabled,omitempty"`
|
||||||
|
|
||||||
|
// 缓存 TTL 强制替换(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||||
|
// 启用后将所有 cache creation tokens 归入指定的 TTL 类型计费
|
||||||
|
CacheTTLOverrideEnabled *bool `json:"cache_ttl_override_enabled,omitempty"`
|
||||||
|
CacheTTLOverrideTarget *string `json:"cache_ttl_override_target,omitempty"`
|
||||||
|
|
||||||
Proxy *Proxy `json:"proxy,omitempty"`
|
Proxy *Proxy `json:"proxy,omitempty"`
|
||||||
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
|
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
|
||||||
|
|
||||||
@@ -191,6 +203,11 @@ type ProxyWithAccountCount struct {
|
|||||||
CountryCode string `json:"country_code,omitempty"`
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
Region string `json:"region,omitempty"`
|
Region string `json:"region,omitempty"`
|
||||||
City string `json:"city,omitempty"`
|
City string `json:"city,omitempty"`
|
||||||
|
QualityStatus string `json:"quality_status,omitempty"`
|
||||||
|
QualityScore *int `json:"quality_score,omitempty"`
|
||||||
|
QualityGrade string `json:"quality_grade,omitempty"`
|
||||||
|
QualitySummary string `json:"quality_summary,omitempty"`
|
||||||
|
QualityChecked *int64 `json:"quality_checked,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyAccountSummary struct {
|
type ProxyAccountSummary struct {
|
||||||
@@ -269,10 +286,14 @@ type UsageLog struct {
|
|||||||
// 图片生成字段
|
// 图片生成字段
|
||||||
ImageCount int `json:"image_count"`
|
ImageCount int `json:"image_count"`
|
||||||
ImageSize *string `json:"image_size"`
|
ImageSize *string `json:"image_size"`
|
||||||
|
MediaType *string `json:"media_type"`
|
||||||
|
|
||||||
// User-Agent
|
// User-Agent
|
||||||
UserAgent *string `json:"user_agent"`
|
UserAgent *string `json:"user_agent"`
|
||||||
|
|
||||||
|
// Cache TTL Override 标记
|
||||||
|
CacheTTLOverridden bool `json:"cache_ttl_overridden"`
|
||||||
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|
||||||
User *User `json:"user,omitempty"`
|
User *User `json:"user,omitempty"`
|
||||||
@@ -374,9 +395,12 @@ type AdminUserSubscription struct {
|
|||||||
|
|
||||||
type BulkAssignResult struct {
|
type BulkAssignResult struct {
|
||||||
SuccessCount int `json:"success_count"`
|
SuccessCount int `json:"success_count"`
|
||||||
|
CreatedCount int `json:"created_count"`
|
||||||
|
ReusedCount int `json:"reused_count"`
|
||||||
FailedCount int `json:"failed_count"`
|
FailedCount int `json:"failed_count"`
|
||||||
Subscriptions []AdminUserSubscription `json:"subscriptions"`
|
Subscriptions []AdminUserSubscription `json:"subscriptions"`
|
||||||
Errors []string `json:"errors"`
|
Errors []string `json:"errors"`
|
||||||
|
Statuses map[string]string `json:"statuses,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromoCode 注册优惠码
|
// PromoCode 注册优惠码
|
||||||
|
|||||||
160
backend/internal/handler/failover_loop.go
Normal file
160
backend/internal/handler/failover_loop.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TempUnscheduler 用于 HandleFailoverError 中同账号重试耗尽后的临时封禁。
|
||||||
|
// GatewayService 隐式实现此接口。
|
||||||
|
type TempUnscheduler interface {
|
||||||
|
TempUnscheduleRetryableError(ctx context.Context, accountID int64, failoverErr *service.UpstreamFailoverError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailoverAction 表示 failover 错误处理后的下一步动作
|
||||||
|
type FailoverAction int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FailoverContinue 继续循环(同账号重试或切换账号,调用方统一 continue)
|
||||||
|
FailoverContinue FailoverAction = iota
|
||||||
|
// FailoverExhausted 切换次数耗尽(调用方应返回错误响应)
|
||||||
|
FailoverExhausted
|
||||||
|
// FailoverCanceled context 已取消(调用方应直接 return)
|
||||||
|
FailoverCanceled
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// maxSameAccountRetries 同账号重试次数上限(针对 RetryableOnSameAccount 错误)
|
||||||
|
maxSameAccountRetries = 2
|
||||||
|
// sameAccountRetryDelay 同账号重试间隔
|
||||||
|
sameAccountRetryDelay = 500 * time.Millisecond
|
||||||
|
// singleAccountBackoffDelay 单账号分组 503 退避重试固定延时。
|
||||||
|
// Service 层在 SingleAccountRetry 模式下已做充分原地重试(最多 3 次、总等待 30s),
|
||||||
|
// Handler 层只需短暂间隔后重新进入 Service 层即可。
|
||||||
|
singleAccountBackoffDelay = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// FailoverState 跨循环迭代共享的 failover 状态
|
||||||
|
type FailoverState struct {
|
||||||
|
SwitchCount int
|
||||||
|
MaxSwitches int
|
||||||
|
FailedAccountIDs map[int64]struct{}
|
||||||
|
SameAccountRetryCount map[int64]int
|
||||||
|
LastFailoverErr *service.UpstreamFailoverError
|
||||||
|
ForceCacheBilling bool
|
||||||
|
hasBoundSession bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFailoverState 创建 failover 状态
|
||||||
|
func NewFailoverState(maxSwitches int, hasBoundSession bool) *FailoverState {
|
||||||
|
return &FailoverState{
|
||||||
|
MaxSwitches: maxSwitches,
|
||||||
|
FailedAccountIDs: make(map[int64]struct{}),
|
||||||
|
SameAccountRetryCount: make(map[int64]int),
|
||||||
|
hasBoundSession: hasBoundSession,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleFailoverError 处理 UpstreamFailoverError,返回下一步动作。
|
||||||
|
// 包含:缓存计费判断、同账号重试、临时封禁、切换计数、Antigravity 延时。
|
||||||
|
func (s *FailoverState) HandleFailoverError(
|
||||||
|
ctx context.Context,
|
||||||
|
gatewayService TempUnscheduler,
|
||||||
|
accountID int64,
|
||||||
|
platform string,
|
||||||
|
failoverErr *service.UpstreamFailoverError,
|
||||||
|
) FailoverAction {
|
||||||
|
s.LastFailoverErr = failoverErr
|
||||||
|
|
||||||
|
// 缓存计费判断
|
||||||
|
if needForceCacheBilling(s.hasBoundSession, failoverErr) {
|
||||||
|
s.ForceCacheBilling = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同账号重试:对 RetryableOnSameAccount 的临时性错误,先在同一账号上重试
|
||||||
|
if failoverErr.RetryableOnSameAccount && s.SameAccountRetryCount[accountID] < maxSameAccountRetries {
|
||||||
|
s.SameAccountRetryCount[accountID]++
|
||||||
|
log.Printf("Account %d: retryable error %d, same-account retry %d/%d",
|
||||||
|
accountID, failoverErr.StatusCode, s.SameAccountRetryCount[accountID], maxSameAccountRetries)
|
||||||
|
if !sleepWithContext(ctx, sameAccountRetryDelay) {
|
||||||
|
return FailoverCanceled
|
||||||
|
}
|
||||||
|
return FailoverContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同账号重试用尽,执行临时封禁
|
||||||
|
if failoverErr.RetryableOnSameAccount {
|
||||||
|
gatewayService.TempUnscheduleRetryableError(ctx, accountID, failoverErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加入失败列表
|
||||||
|
s.FailedAccountIDs[accountID] = struct{}{}
|
||||||
|
|
||||||
|
// 检查是否耗尽
|
||||||
|
if s.SwitchCount >= s.MaxSwitches {
|
||||||
|
return FailoverExhausted
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递增切换计数
|
||||||
|
s.SwitchCount++
|
||||||
|
log.Printf("Account %d: upstream error %d, switching account %d/%d",
|
||||||
|
accountID, failoverErr.StatusCode, s.SwitchCount, s.MaxSwitches)
|
||||||
|
|
||||||
|
// Antigravity 平台换号线性递增延时
|
||||||
|
if platform == service.PlatformAntigravity {
|
||||||
|
delay := time.Duration(s.SwitchCount-1) * time.Second
|
||||||
|
if !sleepWithContext(ctx, delay) {
|
||||||
|
return FailoverCanceled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FailoverContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleSelectionExhausted 处理选号失败(所有候选账号都在排除列表中)时的退避重试决策。
|
||||||
|
// 针对 Antigravity 单账号分组的 503 (MODEL_CAPACITY_EXHAUSTED) 场景:
|
||||||
|
// 清除排除列表、等待退避后重新选号。
|
||||||
|
//
|
||||||
|
// 返回 FailoverContinue 时,调用方应设置 SingleAccountRetry context 并 continue。
|
||||||
|
// 返回 FailoverExhausted 时,调用方应返回错误响应。
|
||||||
|
// 返回 FailoverCanceled 时,调用方应直接 return。
|
||||||
|
func (s *FailoverState) HandleSelectionExhausted(ctx context.Context) FailoverAction {
|
||||||
|
if s.LastFailoverErr != nil &&
|
||||||
|
s.LastFailoverErr.StatusCode == http.StatusServiceUnavailable &&
|
||||||
|
s.SwitchCount <= s.MaxSwitches {
|
||||||
|
|
||||||
|
log.Printf("Antigravity single-account 503 backoff: waiting %v before retry (attempt %d)",
|
||||||
|
singleAccountBackoffDelay, s.SwitchCount)
|
||||||
|
if !sleepWithContext(ctx, singleAccountBackoffDelay) {
|
||||||
|
return FailoverCanceled
|
||||||
|
}
|
||||||
|
log.Printf("Antigravity single-account 503 retry: clearing failed accounts, retry %d/%d",
|
||||||
|
s.SwitchCount, s.MaxSwitches)
|
||||||
|
s.FailedAccountIDs = make(map[int64]struct{})
|
||||||
|
return FailoverContinue
|
||||||
|
}
|
||||||
|
return FailoverExhausted
|
||||||
|
}
|
||||||
|
|
||||||
|
// needForceCacheBilling 判断 failover 时是否需要强制缓存计费。
|
||||||
|
// 粘性会话切换账号、或上游明确标记时,将 input_tokens 转为 cache_read 计费。
|
||||||
|
func needForceCacheBilling(hasBoundSession bool, failoverErr *service.UpstreamFailoverError) bool {
|
||||||
|
return hasBoundSession || (failoverErr != nil && failoverErr.ForceCacheBilling)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sleepWithContext 等待指定时长,返回 false 表示 context 已取消。
|
||||||
|
func sleepWithContext(ctx context.Context, d time.Duration) bool {
|
||||||
|
if d <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false
|
||||||
|
case <-time.After(d):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
732
backend/internal/handler/failover_loop_test.go
Normal file
732
backend/internal/handler/failover_loop_test.go
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mock
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// mockTempUnscheduler 记录 TempUnscheduleRetryableError 的调用信息。
|
||||||
|
type mockTempUnscheduler struct {
|
||||||
|
calls []tempUnscheduleCall
|
||||||
|
}
|
||||||
|
|
||||||
|
type tempUnscheduleCall struct {
|
||||||
|
accountID int64
|
||||||
|
failoverErr *service.UpstreamFailoverError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTempUnscheduler) TempUnscheduleRetryableError(_ context.Context, accountID int64, failoverErr *service.UpstreamFailoverError) {
|
||||||
|
m.calls = append(m.calls, tempUnscheduleCall{accountID: accountID, failoverErr: failoverErr})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func newTestFailoverErr(statusCode int, retryable, forceBilling bool) *service.UpstreamFailoverError {
|
||||||
|
return &service.UpstreamFailoverError{
|
||||||
|
StatusCode: statusCode,
|
||||||
|
RetryableOnSameAccount: retryable,
|
||||||
|
ForceCacheBilling: forceBilling,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// NewFailoverState 测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestNewFailoverState(t *testing.T) {
|
||||||
|
t.Run("初始化字段正确", func(t *testing.T) {
|
||||||
|
fs := NewFailoverState(5, true)
|
||||||
|
require.Equal(t, 5, fs.MaxSwitches)
|
||||||
|
require.Equal(t, 0, fs.SwitchCount)
|
||||||
|
require.NotNil(t, fs.FailedAccountIDs)
|
||||||
|
require.Empty(t, fs.FailedAccountIDs)
|
||||||
|
require.NotNil(t, fs.SameAccountRetryCount)
|
||||||
|
require.Empty(t, fs.SameAccountRetryCount)
|
||||||
|
require.Nil(t, fs.LastFailoverErr)
|
||||||
|
require.False(t, fs.ForceCacheBilling)
|
||||||
|
require.True(t, fs.hasBoundSession)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("无绑定会话", func(t *testing.T) {
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
require.Equal(t, 3, fs.MaxSwitches)
|
||||||
|
require.False(t, fs.hasBoundSession)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("零最大切换次数", func(t *testing.T) {
|
||||||
|
fs := NewFailoverState(0, false)
|
||||||
|
require.Equal(t, 0, fs.MaxSwitches)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// sleepWithContext 测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestSleepWithContext(t *testing.T) {
|
||||||
|
t.Run("零时长立即返回true", func(t *testing.T) {
|
||||||
|
start := time.Now()
|
||||||
|
ok := sleepWithContext(context.Background(), 0)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Less(t, time.Since(start), 50*time.Millisecond)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("负时长立即返回true", func(t *testing.T) {
|
||||||
|
start := time.Now()
|
||||||
|
ok := sleepWithContext(context.Background(), -1*time.Second)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Less(t, time.Since(start), 50*time.Millisecond)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("正常等待后返回true", func(t *testing.T) {
|
||||||
|
start := time.Now()
|
||||||
|
ok := sleepWithContext(context.Background(), 50*time.Millisecond)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.GreaterOrEqual(t, elapsed, 40*time.Millisecond)
|
||||||
|
require.Less(t, elapsed, 500*time.Millisecond)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("已取消context立即返回false", func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
ok := sleepWithContext(ctx, 5*time.Second)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Less(t, time.Since(start), 50*time.Millisecond)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("等待期间context取消返回false", func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
time.Sleep(30 * time.Millisecond)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
ok := sleepWithContext(ctx, 5*time.Second)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Less(t, elapsed, 500*time.Millisecond)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HandleFailoverError — 基本切换流程
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandleFailoverError_BasicSwitch(t *testing.T) {
|
||||||
|
t.Run("非重试错误_非Antigravity_直接切换", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(500, false, false)
|
||||||
|
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SwitchCount)
|
||||||
|
require.Contains(t, fs.FailedAccountIDs, int64(100))
|
||||||
|
require.Equal(t, err, fs.LastFailoverErr)
|
||||||
|
require.False(t, fs.ForceCacheBilling)
|
||||||
|
require.Empty(t, mock.calls, "不应调用 TempUnschedule")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("非重试错误_Antigravity_第一次切换无延迟", func(t *testing.T) {
|
||||||
|
// switchCount 从 0→1 时,sleepFailoverDelay(ctx, 1) 的延时 = (1-1)*1s = 0
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(500, false, false)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, service.PlatformAntigravity, err)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SwitchCount)
|
||||||
|
require.Less(t, elapsed, 200*time.Millisecond, "第一次切换延迟应为 0")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("非重试错误_Antigravity_第二次切换有1秒延迟", func(t *testing.T) {
|
||||||
|
// switchCount 从 1→2 时,sleepFailoverDelay(ctx, 2) 的延时 = (2-1)*1s = 1s
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
fs.SwitchCount = 1 // 模拟已切换一次
|
||||||
|
|
||||||
|
err := newTestFailoverErr(500, false, false)
|
||||||
|
start := time.Now()
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 200, service.PlatformAntigravity, err)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 2, fs.SwitchCount)
|
||||||
|
require.GreaterOrEqual(t, elapsed, 800*time.Millisecond, "第二次切换延迟应约 1s")
|
||||||
|
require.Less(t, elapsed, 3*time.Second)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("连续切换直到耗尽", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(2, false)
|
||||||
|
|
||||||
|
// 第一次切换:0→1
|
||||||
|
err1 := newTestFailoverErr(500, false, false)
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", err1)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SwitchCount)
|
||||||
|
|
||||||
|
// 第二次切换:1→2
|
||||||
|
err2 := newTestFailoverErr(502, false, false)
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 200, "openai", err2)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 2, fs.SwitchCount)
|
||||||
|
|
||||||
|
// 第三次已耗尽:SwitchCount(2) >= MaxSwitches(2)
|
||||||
|
err3 := newTestFailoverErr(503, false, false)
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 300, "openai", err3)
|
||||||
|
require.Equal(t, FailoverExhausted, action)
|
||||||
|
require.Equal(t, 2, fs.SwitchCount, "耗尽时不应继续递增")
|
||||||
|
|
||||||
|
// 验证失败账号列表
|
||||||
|
require.Len(t, fs.FailedAccountIDs, 3)
|
||||||
|
require.Contains(t, fs.FailedAccountIDs, int64(100))
|
||||||
|
require.Contains(t, fs.FailedAccountIDs, int64(200))
|
||||||
|
require.Contains(t, fs.FailedAccountIDs, int64(300))
|
||||||
|
|
||||||
|
// LastFailoverErr 应为最后一次的错误
|
||||||
|
require.Equal(t, err3, fs.LastFailoverErr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MaxSwitches为0时首次即耗尽", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(0, false)
|
||||||
|
err := newTestFailoverErr(500, false, false)
|
||||||
|
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Equal(t, FailoverExhausted, action)
|
||||||
|
require.Equal(t, 0, fs.SwitchCount)
|
||||||
|
require.Contains(t, fs.FailedAccountIDs, int64(100))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HandleFailoverError — 缓存计费 (ForceCacheBilling)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandleFailoverError_CacheBilling(t *testing.T) {
|
||||||
|
t.Run("hasBoundSession为true时设置ForceCacheBilling", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, true) // hasBoundSession=true
|
||||||
|
err := newTestFailoverErr(500, false, false)
|
||||||
|
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.True(t, fs.ForceCacheBilling)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failoverErr.ForceCacheBilling为true时设置", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(500, false, true) // ForceCacheBilling=true
|
||||||
|
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.True(t, fs.ForceCacheBilling)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("两者均为false时不设置", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(500, false, false)
|
||||||
|
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.False(t, fs.ForceCacheBilling)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("一旦设置不会被后续错误重置", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
|
||||||
|
// 第一次:ForceCacheBilling=true → 设置
|
||||||
|
err1 := newTestFailoverErr(500, false, true)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err1)
|
||||||
|
require.True(t, fs.ForceCacheBilling)
|
||||||
|
|
||||||
|
// 第二次:ForceCacheBilling=false → 仍然保持 true
|
||||||
|
err2 := newTestFailoverErr(502, false, false)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 200, "openai", err2)
|
||||||
|
require.True(t, fs.ForceCacheBilling, "ForceCacheBilling 一旦设置不应被重置")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HandleFailoverError — 同账号重试 (RetryableOnSameAccount)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandleFailoverError_SameAccountRetry(t *testing.T) {
|
||||||
|
t.Run("第一次重试返回FailoverContinue", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(400, true, false)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SameAccountRetryCount[100])
|
||||||
|
require.Equal(t, 0, fs.SwitchCount, "同账号重试不应增加切换计数")
|
||||||
|
require.NotContains(t, fs.FailedAccountIDs, int64(100), "同账号重试不应加入失败列表")
|
||||||
|
require.Empty(t, mock.calls, "同账号重试期间不应调用 TempUnschedule")
|
||||||
|
// 验证等待了 sameAccountRetryDelay (500ms)
|
||||||
|
require.GreaterOrEqual(t, elapsed, 400*time.Millisecond)
|
||||||
|
require.Less(t, elapsed, 2*time.Second)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("第二次重试仍返回FailoverContinue", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(400, true, false)
|
||||||
|
|
||||||
|
// 第一次
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SameAccountRetryCount[100])
|
||||||
|
|
||||||
|
// 第二次
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 2, fs.SameAccountRetryCount[100])
|
||||||
|
|
||||||
|
require.Empty(t, mock.calls, "两次重试期间均不应调用 TempUnschedule")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("第三次重试耗尽_触发TempUnschedule并切换", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(400, true, false)
|
||||||
|
|
||||||
|
// 第一次、第二次重试
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Equal(t, 2, fs.SameAccountRetryCount[100])
|
||||||
|
|
||||||
|
// 第三次:重试已达到 maxSameAccountRetries(2),应切换账号
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SwitchCount)
|
||||||
|
require.Contains(t, fs.FailedAccountIDs, int64(100))
|
||||||
|
|
||||||
|
// 验证 TempUnschedule 被调用
|
||||||
|
require.Len(t, mock.calls, 1)
|
||||||
|
require.Equal(t, int64(100), mock.calls[0].accountID)
|
||||||
|
require.Equal(t, err, mock.calls[0].failoverErr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("不同账号独立跟踪重试次数", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(5, false)
|
||||||
|
err := newTestFailoverErr(400, true, false)
|
||||||
|
|
||||||
|
// 账号 100 第一次重试
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SameAccountRetryCount[100])
|
||||||
|
|
||||||
|
// 账号 200 第一次重试(独立计数)
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 200, "openai", err)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SameAccountRetryCount[200])
|
||||||
|
require.Equal(t, 1, fs.SameAccountRetryCount[100], "账号 100 的计数不应受影响")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("重试耗尽后再次遇到同账号_直接切换", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(5, false)
|
||||||
|
err := newTestFailoverErr(400, true, false)
|
||||||
|
|
||||||
|
// 耗尽账号 100 的重试
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
// 第三次: 重试耗尽 → 切换
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
|
||||||
|
// 再次遇到账号 100,计数仍为 2,条件不满足 → 直接切换
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Len(t, mock.calls, 2, "第二次耗尽也应调用 TempUnschedule")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HandleFailoverError — TempUnschedule 调用验证
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandleFailoverError_TempUnschedule(t *testing.T) {
|
||||||
|
t.Run("非重试错误不调用TempUnschedule", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(500, false, false) // RetryableOnSameAccount=false
|
||||||
|
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Empty(t, mock.calls)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("重试错误耗尽后调用TempUnschedule_传入正确参数", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(502, true, false)
|
||||||
|
|
||||||
|
// 耗尽重试
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 42, "openai", err)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 42, "openai", err)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 42, "openai", err)
|
||||||
|
|
||||||
|
require.Len(t, mock.calls, 1)
|
||||||
|
require.Equal(t, int64(42), mock.calls[0].accountID)
|
||||||
|
require.Equal(t, 502, mock.calls[0].failoverErr.StatusCode)
|
||||||
|
require.True(t, mock.calls[0].failoverErr.RetryableOnSameAccount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HandleFailoverError — Context 取消
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandleFailoverError_ContextCanceled(t *testing.T) {
|
||||||
|
t.Run("同账号重试sleep期间context取消", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(400, true, false)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel() // 立即取消
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
action := fs.HandleFailoverError(ctx, mock, 100, "openai", err)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
require.Equal(t, FailoverCanceled, action)
|
||||||
|
require.Less(t, elapsed, 100*time.Millisecond, "应立即返回")
|
||||||
|
// 重试计数仍应递增
|
||||||
|
require.Equal(t, 1, fs.SameAccountRetryCount[100])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Antigravity延迟期间context取消", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
fs.SwitchCount = 1 // 下一次 switchCount=2 → delay = 1s
|
||||||
|
err := newTestFailoverErr(500, false, false)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel() // 立即取消
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
action := fs.HandleFailoverError(ctx, mock, 100, service.PlatformAntigravity, err)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
require.Equal(t, FailoverCanceled, action)
|
||||||
|
require.Less(t, elapsed, 100*time.Millisecond, "应立即返回而非等待 1s")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HandleFailoverError — FailedAccountIDs 跟踪
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandleFailoverError_FailedAccountIDs(t *testing.T) {
|
||||||
|
t.Run("切换时添加到失败列表", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", newTestFailoverErr(500, false, false))
|
||||||
|
require.Contains(t, fs.FailedAccountIDs, int64(100))
|
||||||
|
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 200, "openai", newTestFailoverErr(502, false, false))
|
||||||
|
require.Contains(t, fs.FailedAccountIDs, int64(200))
|
||||||
|
require.Len(t, fs.FailedAccountIDs, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("耗尽时也添加到失败列表", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(0, false)
|
||||||
|
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", newTestFailoverErr(500, false, false))
|
||||||
|
require.Equal(t, FailoverExhausted, action)
|
||||||
|
require.Contains(t, fs.FailedAccountIDs, int64(100))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("同账号重试期间不添加到失败列表", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", newTestFailoverErr(400, true, false))
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.NotContains(t, fs.FailedAccountIDs, int64(100))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("同一账号多次切换不重复添加", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(5, false)
|
||||||
|
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", newTestFailoverErr(500, false, false))
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", newTestFailoverErr(500, false, false))
|
||||||
|
require.Len(t, fs.FailedAccountIDs, 1, "map 天然去重")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HandleFailoverError — LastFailoverErr 更新
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandleFailoverError_LastFailoverErr(t *testing.T) {
|
||||||
|
t.Run("每次调用都更新LastFailoverErr", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
|
||||||
|
err1 := newTestFailoverErr(500, false, false)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err1)
|
||||||
|
require.Equal(t, err1, fs.LastFailoverErr)
|
||||||
|
|
||||||
|
err2 := newTestFailoverErr(502, false, false)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 200, "openai", err2)
|
||||||
|
require.Equal(t, err2, fs.LastFailoverErr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("同账号重试时也更新LastFailoverErr", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
|
||||||
|
err := newTestFailoverErr(400, true, false)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Equal(t, err, fs.LastFailoverErr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HandleFailoverError — 综合集成场景
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandleFailoverError_IntegrationScenario(t *testing.T) {
|
||||||
|
t.Run("模拟完整failover流程_多账号混合重试与切换", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, true) // hasBoundSession=true
|
||||||
|
|
||||||
|
// 1. 账号 100 遇到可重试错误,同账号重试 2 次
|
||||||
|
retryErr := newTestFailoverErr(400, true, false)
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", retryErr)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.True(t, fs.ForceCacheBilling, "hasBoundSession=true 应设置 ForceCacheBilling")
|
||||||
|
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 100, "openai", retryErr)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
|
||||||
|
// 2. 账号 100 重试耗尽 → TempUnschedule + 切换
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 100, "openai", retryErr)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SwitchCount)
|
||||||
|
require.Len(t, mock.calls, 1)
|
||||||
|
|
||||||
|
// 3. 账号 200 遇到不可重试错误 → 直接切换
|
||||||
|
switchErr := newTestFailoverErr(500, false, false)
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 200, "openai", switchErr)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 2, fs.SwitchCount)
|
||||||
|
|
||||||
|
// 4. 账号 300 遇到不可重试错误 → 再切换
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 300, "openai", switchErr)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 3, fs.SwitchCount)
|
||||||
|
|
||||||
|
// 5. 账号 400 → 已耗尽 (SwitchCount=3 >= MaxSwitches=3)
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 400, "openai", switchErr)
|
||||||
|
require.Equal(t, FailoverExhausted, action)
|
||||||
|
|
||||||
|
// 最终状态验证
|
||||||
|
require.Equal(t, 3, fs.SwitchCount, "耗尽时不再递增")
|
||||||
|
require.Len(t, fs.FailedAccountIDs, 4, "4个不同账号都在失败列表中")
|
||||||
|
require.True(t, fs.ForceCacheBilling)
|
||||||
|
require.Len(t, mock.calls, 1, "只有账号 100 触发了 TempUnschedule")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("模拟Antigravity平台完整流程", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(2, false)
|
||||||
|
|
||||||
|
err := newTestFailoverErr(500, false, false)
|
||||||
|
|
||||||
|
// 第一次切换:delay = 0s
|
||||||
|
start := time.Now()
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, service.PlatformAntigravity, err)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Less(t, elapsed, 200*time.Millisecond, "第一次切换延迟为 0")
|
||||||
|
|
||||||
|
// 第二次切换:delay = 1s
|
||||||
|
start = time.Now()
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 200, service.PlatformAntigravity, err)
|
||||||
|
elapsed = time.Since(start)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.GreaterOrEqual(t, elapsed, 800*time.Millisecond, "第二次切换延迟约 1s")
|
||||||
|
|
||||||
|
// 第三次:耗尽(无延迟,因为在检查延迟之前就返回了)
|
||||||
|
start = time.Now()
|
||||||
|
action = fs.HandleFailoverError(context.Background(), mock, 300, service.PlatformAntigravity, err)
|
||||||
|
elapsed = time.Since(start)
|
||||||
|
require.Equal(t, FailoverExhausted, action)
|
||||||
|
require.Less(t, elapsed, 200*time.Millisecond, "耗尽时不应有延迟")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ForceCacheBilling通过错误标志设置", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false) // hasBoundSession=false
|
||||||
|
|
||||||
|
// 第一次:ForceCacheBilling=false
|
||||||
|
err1 := newTestFailoverErr(500, false, false)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 100, "openai", err1)
|
||||||
|
require.False(t, fs.ForceCacheBilling)
|
||||||
|
|
||||||
|
// 第二次:ForceCacheBilling=true(Antigravity 粘性会话切换)
|
||||||
|
err2 := newTestFailoverErr(500, false, true)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 200, "openai", err2)
|
||||||
|
require.True(t, fs.ForceCacheBilling, "错误标志应触发 ForceCacheBilling")
|
||||||
|
|
||||||
|
// 第三次:ForceCacheBilling=false,但状态仍保持 true
|
||||||
|
err3 := newTestFailoverErr(500, false, false)
|
||||||
|
fs.HandleFailoverError(context.Background(), mock, 300, "openai", err3)
|
||||||
|
require.True(t, fs.ForceCacheBilling, "不应重置")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HandleFailoverError — 边界条件
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandleFailoverError_EdgeCases(t *testing.T) {
|
||||||
|
t.Run("StatusCode为0的错误也能正常处理", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(0, false, false)
|
||||||
|
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "openai", err)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AccountID为0也能正常跟踪", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(500, true, false)
|
||||||
|
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 0, "openai", err)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SameAccountRetryCount[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("负AccountID也能正常跟踪", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
err := newTestFailoverErr(500, true, false)
|
||||||
|
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, -1, "openai", err)
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Equal(t, 1, fs.SameAccountRetryCount[-1])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("空平台名称不触发Antigravity延迟", func(t *testing.T) {
|
||||||
|
mock := &mockTempUnscheduler{}
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
fs.SwitchCount = 1
|
||||||
|
err := newTestFailoverErr(500, false, false)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
action := fs.HandleFailoverError(context.Background(), mock, 100, "", err)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Less(t, elapsed, 200*time.Millisecond, "空平台不应触发 Antigravity 延迟")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// HandleSelectionExhausted 测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestHandleSelectionExhausted(t *testing.T) {
|
||||||
|
t.Run("无LastFailoverErr时返回Exhausted", func(t *testing.T) {
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
// LastFailoverErr 为 nil
|
||||||
|
|
||||||
|
action := fs.HandleSelectionExhausted(context.Background())
|
||||||
|
require.Equal(t, FailoverExhausted, action)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("非503错误返回Exhausted", func(t *testing.T) {
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
fs.LastFailoverErr = newTestFailoverErr(500, false, false)
|
||||||
|
|
||||||
|
action := fs.HandleSelectionExhausted(context.Background())
|
||||||
|
require.Equal(t, FailoverExhausted, action)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("503且未耗尽_等待后返回Continue并清除失败列表", func(t *testing.T) {
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
fs.LastFailoverErr = newTestFailoverErr(503, false, false)
|
||||||
|
fs.FailedAccountIDs[100] = struct{}{}
|
||||||
|
fs.SwitchCount = 1
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
action := fs.HandleSelectionExhausted(context.Background())
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
require.Empty(t, fs.FailedAccountIDs, "应清除失败账号列表")
|
||||||
|
require.GreaterOrEqual(t, elapsed, 1500*time.Millisecond, "应等待约 2s")
|
||||||
|
require.Less(t, elapsed, 5*time.Second)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("503但SwitchCount已超过MaxSwitches_返回Exhausted", func(t *testing.T) {
|
||||||
|
fs := NewFailoverState(2, false)
|
||||||
|
fs.LastFailoverErr = newTestFailoverErr(503, false, false)
|
||||||
|
fs.SwitchCount = 3 // > MaxSwitches(2)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
action := fs.HandleSelectionExhausted(context.Background())
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
require.Equal(t, FailoverExhausted, action)
|
||||||
|
require.Less(t, elapsed, 100*time.Millisecond, "不应等待")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("503但context已取消_返回Canceled", func(t *testing.T) {
|
||||||
|
fs := NewFailoverState(3, false)
|
||||||
|
fs.LastFailoverErr = newTestFailoverErr(503, false, false)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
action := fs.HandleSelectionExhausted(ctx)
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
require.Equal(t, FailoverCanceled, action)
|
||||||
|
require.Less(t, elapsed, 100*time.Millisecond, "应立即返回")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("503且SwitchCount等于MaxSwitches_仍可重试", func(t *testing.T) {
|
||||||
|
fs := NewFailoverState(2, false)
|
||||||
|
fs.LastFailoverErr = newTestFailoverErr(503, false, false)
|
||||||
|
fs.SwitchCount = 2 // == MaxSwitches,条件是 <=,仍可重试
|
||||||
|
|
||||||
|
action := fs.HandleSelectionExhausted(context.Background())
|
||||||
|
require.Equal(t, FailoverContinue, action)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -19,11 +18,13 @@ import (
|
|||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
|
||||||
pkgerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
pkgerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/ip"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/ip"
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||||
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GatewayHandler handles API gateway requests
|
// GatewayHandler handles API gateway requests
|
||||||
@@ -35,10 +36,12 @@ type GatewayHandler struct {
|
|||||||
billingCacheService *service.BillingCacheService
|
billingCacheService *service.BillingCacheService
|
||||||
usageService *service.UsageService
|
usageService *service.UsageService
|
||||||
apiKeyService *service.APIKeyService
|
apiKeyService *service.APIKeyService
|
||||||
|
usageRecordWorkerPool *service.UsageRecordWorkerPool
|
||||||
errorPassthroughService *service.ErrorPassthroughService
|
errorPassthroughService *service.ErrorPassthroughService
|
||||||
concurrencyHelper *ConcurrencyHelper
|
concurrencyHelper *ConcurrencyHelper
|
||||||
maxAccountSwitches int
|
maxAccountSwitches int
|
||||||
maxAccountSwitchesGemini int
|
maxAccountSwitchesGemini int
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGatewayHandler creates a new GatewayHandler
|
// NewGatewayHandler creates a new GatewayHandler
|
||||||
@@ -51,6 +54,7 @@ func NewGatewayHandler(
|
|||||||
billingCacheService *service.BillingCacheService,
|
billingCacheService *service.BillingCacheService,
|
||||||
usageService *service.UsageService,
|
usageService *service.UsageService,
|
||||||
apiKeyService *service.APIKeyService,
|
apiKeyService *service.APIKeyService,
|
||||||
|
usageRecordWorkerPool *service.UsageRecordWorkerPool,
|
||||||
errorPassthroughService *service.ErrorPassthroughService,
|
errorPassthroughService *service.ErrorPassthroughService,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
) *GatewayHandler {
|
) *GatewayHandler {
|
||||||
@@ -74,10 +78,12 @@ func NewGatewayHandler(
|
|||||||
billingCacheService: billingCacheService,
|
billingCacheService: billingCacheService,
|
||||||
usageService: usageService,
|
usageService: usageService,
|
||||||
apiKeyService: apiKeyService,
|
apiKeyService: apiKeyService,
|
||||||
|
usageRecordWorkerPool: usageRecordWorkerPool,
|
||||||
errorPassthroughService: errorPassthroughService,
|
errorPassthroughService: errorPassthroughService,
|
||||||
concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatClaude, pingInterval),
|
concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatClaude, pingInterval),
|
||||||
maxAccountSwitches: maxAccountSwitches,
|
maxAccountSwitches: maxAccountSwitches,
|
||||||
maxAccountSwitchesGemini: maxAccountSwitchesGemini,
|
maxAccountSwitchesGemini: maxAccountSwitchesGemini,
|
||||||
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +102,13 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
|
h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
reqLog := requestLogger(
|
||||||
|
c,
|
||||||
|
"handler.gateway.messages",
|
||||||
|
zap.Int64("user_id", subject.UserID),
|
||||||
|
zap.Int64("api_key_id", apiKey.ID),
|
||||||
|
zap.Any("group_id", apiKey.GroupID),
|
||||||
|
)
|
||||||
|
|
||||||
// 读取请求体
|
// 读取请求体
|
||||||
body, err := io.ReadAll(c.Request.Body)
|
body, err := io.ReadAll(c.Request.Body)
|
||||||
@@ -122,6 +135,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
reqModel := parsedReq.Model
|
reqModel := parsedReq.Model
|
||||||
reqStream := parsedReq.Stream
|
reqStream := parsedReq.Stream
|
||||||
|
reqLog = reqLog.With(zap.String("model", reqModel), zap.Bool("stream", reqStream))
|
||||||
|
|
||||||
// 设置 max_tokens=1 + haiku 探测请求标识到 context 中
|
// 设置 max_tokens=1 + haiku 探测请求标识到 context 中
|
||||||
// 必须在 SetClaudeCodeClientContext 之前设置,因为 ClaudeCodeValidator 需要读取此标识进行绕过判断
|
// 必须在 SetClaudeCodeClientContext 之前设置,因为 ClaudeCodeValidator 需要读取此标识进行绕过判断
|
||||||
@@ -161,9 +175,10 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
canWait, err := h.concurrencyHelper.IncrementWaitCount(c.Request.Context(), subject.UserID, maxWait)
|
canWait, err := h.concurrencyHelper.IncrementWaitCount(c.Request.Context(), subject.UserID, maxWait)
|
||||||
waitCounted := false
|
waitCounted := false
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Increment wait count failed: %v", err)
|
reqLog.Warn("gateway.user_wait_counter_increment_failed", zap.Error(err))
|
||||||
// On error, allow request to proceed
|
// On error, allow request to proceed
|
||||||
} else if !canWait {
|
} else if !canWait {
|
||||||
|
reqLog.Info("gateway.user_wait_queue_full", zap.Int("max_wait", maxWait))
|
||||||
h.errorResponse(c, http.StatusTooManyRequests, "rate_limit_error", "Too many pending requests, please retry later")
|
h.errorResponse(c, http.StatusTooManyRequests, "rate_limit_error", "Too many pending requests, please retry later")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -180,7 +195,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
// 1. 首先获取用户并发槽位
|
// 1. 首先获取用户并发槽位
|
||||||
userReleaseFunc, err := h.concurrencyHelper.AcquireUserSlotWithWait(c, subject.UserID, subject.Concurrency, reqStream, &streamStarted)
|
userReleaseFunc, err := h.concurrencyHelper.AcquireUserSlotWithWait(c, subject.UserID, subject.Concurrency, reqStream, &streamStarted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("User concurrency acquire failed: %v", err)
|
reqLog.Warn("gateway.user_slot_acquire_failed", zap.Error(err))
|
||||||
h.handleConcurrencyError(c, err, "user", streamStarted)
|
h.handleConcurrencyError(c, err, "user", streamStarted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -197,7 +212,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
|
|
||||||
// 2. 【新增】Wait后二次检查余额/订阅
|
// 2. 【新增】Wait后二次检查余额/订阅
|
||||||
if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription); err != nil {
|
if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), apiKey.User, apiKey, apiKey.Group, subscription); err != nil {
|
||||||
log.Printf("Billing eligibility check failed after wait: %v", err)
|
reqLog.Info("gateway.billing_eligibility_check_failed", zap.Error(err))
|
||||||
status, code, message := billingErrorDetails(err)
|
status, code, message := billingErrorDetails(err)
|
||||||
h.handleStreamingAwareError(c, status, code, message, streamStarted)
|
h.handleStreamingAwareError(c, status, code, message, streamStarted)
|
||||||
return
|
return
|
||||||
@@ -227,16 +242,21 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
var sessionBoundAccountID int64
|
var sessionBoundAccountID int64
|
||||||
if sessionKey != "" {
|
if sessionKey != "" {
|
||||||
sessionBoundAccountID, _ = h.gatewayService.GetCachedSessionAccountID(c.Request.Context(), apiKey.GroupID, sessionKey)
|
sessionBoundAccountID, _ = h.gatewayService.GetCachedSessionAccountID(c.Request.Context(), apiKey.GroupID, sessionKey)
|
||||||
|
if sessionBoundAccountID > 0 {
|
||||||
|
prefetchedGroupID := int64(0)
|
||||||
|
if apiKey.GroupID != nil {
|
||||||
|
prefetchedGroupID = *apiKey.GroupID
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(c.Request.Context(), ctxkey.PrefetchedStickyAccountID, sessionBoundAccountID)
|
||||||
|
ctx = context.WithValue(ctx, ctxkey.PrefetchedStickyGroupID, prefetchedGroupID)
|
||||||
|
c.Request = c.Request.WithContext(ctx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 判断是否真的绑定了粘性会话:有 sessionKey 且已经绑定到某个账号
|
// 判断是否真的绑定了粘性会话:有 sessionKey 且已经绑定到某个账号
|
||||||
hasBoundSession := sessionKey != "" && sessionBoundAccountID > 0
|
hasBoundSession := sessionKey != "" && sessionBoundAccountID > 0
|
||||||
|
|
||||||
if platform == service.PlatformGemini {
|
if platform == service.PlatformGemini {
|
||||||
maxAccountSwitches := h.maxAccountSwitchesGemini
|
fs := NewFailoverState(h.maxAccountSwitchesGemini, hasBoundSession)
|
||||||
switchCount := 0
|
|
||||||
failedAccountIDs := make(map[int64]struct{})
|
|
||||||
var lastFailoverErr *service.UpstreamFailoverError
|
|
||||||
var forceCacheBilling bool // 粘性会话切换时的缓存计费标记
|
|
||||||
|
|
||||||
// 单账号分组提前设置 SingleAccountRetry 标记,让 Service 层首次 503 就不设模型限流标记。
|
// 单账号分组提前设置 SingleAccountRetry 标记,让 Service 层首次 503 就不设模型限流标记。
|
||||||
// 避免单账号分组收到 503 (MODEL_CAPACITY_EXHAUSTED) 时设 29s 限流,导致后续请求连续快速失败。
|
// 避免单账号分组收到 503 (MODEL_CAPACITY_EXHAUSTED) 时设 29s 限流,导致后续请求连续快速失败。
|
||||||
@@ -246,34 +266,31 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), apiKey.GroupID, sessionKey, reqModel, failedAccountIDs, "") // Gemini 不使用会话限制
|
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), apiKey.GroupID, sessionKey, reqModel, fs.FailedAccountIDs, "") // Gemini 不使用会话限制
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(failedAccountIDs) == 0 {
|
if len(fs.FailedAccountIDs) == 0 {
|
||||||
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted)
|
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Antigravity 单账号退避重试:分组内没有其他可用账号时,
|
action := fs.HandleSelectionExhausted(c.Request.Context())
|
||||||
// 对 503 错误不直接返回,而是清除排除列表、等待退避后重试同一个账号。
|
switch action {
|
||||||
// 谷歌上游 503 (MODEL_CAPACITY_EXHAUSTED) 通常是暂时性的,等几秒就能恢复。
|
case FailoverContinue:
|
||||||
if lastFailoverErr != nil && lastFailoverErr.StatusCode == http.StatusServiceUnavailable && switchCount <= maxAccountSwitches {
|
ctx := context.WithValue(c.Request.Context(), ctxkey.SingleAccountRetry, true)
|
||||||
if sleepAntigravitySingleAccountBackoff(c.Request.Context(), switchCount) {
|
c.Request = c.Request.WithContext(ctx)
|
||||||
log.Printf("Antigravity single-account 503 retry: clearing failed accounts, retry %d/%d", switchCount, maxAccountSwitches)
|
continue
|
||||||
failedAccountIDs = make(map[int64]struct{})
|
case FailoverCanceled:
|
||||||
// 设置 context 标记,让 Service 层预检查等待限流过期而非直接切换
|
return
|
||||||
ctx := context.WithValue(c.Request.Context(), ctxkey.SingleAccountRetry, true)
|
default: // FailoverExhausted
|
||||||
c.Request = c.Request.WithContext(ctx)
|
if fs.LastFailoverErr != nil {
|
||||||
continue
|
h.handleFailoverExhausted(c, fs.LastFailoverErr, service.PlatformGemini, streamStarted)
|
||||||
|
} else {
|
||||||
|
h.handleFailoverExhaustedSimple(c, 502, streamStarted)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if lastFailoverErr != nil {
|
|
||||||
h.handleFailoverExhausted(c, lastFailoverErr, service.PlatformGemini, streamStarted)
|
|
||||||
} else {
|
|
||||||
h.handleFailoverExhaustedSimple(c, 502, streamStarted)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
account := selection.Account
|
account := selection.Account
|
||||||
setOpsSelectedAccount(c, account.ID)
|
setOpsSelectedAccount(c, account.ID, account.Platform)
|
||||||
|
|
||||||
// 检查请求拦截(预热请求、SUGGESTION MODE等)
|
// 检查请求拦截(预热请求、SUGGESTION MODE等)
|
||||||
if account.IsInterceptWarmupEnabled() {
|
if account.IsInterceptWarmupEnabled() {
|
||||||
@@ -301,21 +318,24 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
accountWaitCounted := false
|
accountWaitCounted := false
|
||||||
canWait, err := h.concurrencyHelper.IncrementAccountWaitCount(c.Request.Context(), account.ID, selection.WaitPlan.MaxWaiting)
|
canWait, err := h.concurrencyHelper.IncrementAccountWaitCount(c.Request.Context(), account.ID, selection.WaitPlan.MaxWaiting)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Increment account wait count failed: %v", err)
|
reqLog.Warn("gateway.account_wait_counter_increment_failed", zap.Int64("account_id", account.ID), zap.Error(err))
|
||||||
} else if !canWait {
|
} else if !canWait {
|
||||||
log.Printf("Account wait queue full: account=%d", account.ID)
|
reqLog.Info("gateway.account_wait_queue_full",
|
||||||
|
zap.Int64("account_id", account.ID),
|
||||||
|
zap.Int("max_waiting", selection.WaitPlan.MaxWaiting),
|
||||||
|
)
|
||||||
h.handleStreamingAwareError(c, http.StatusTooManyRequests, "rate_limit_error", "Too many pending requests, please retry later", streamStarted)
|
h.handleStreamingAwareError(c, http.StatusTooManyRequests, "rate_limit_error", "Too many pending requests, please retry later", streamStarted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err == nil && canWait {
|
if err == nil && canWait {
|
||||||
accountWaitCounted = true
|
accountWaitCounted = true
|
||||||
}
|
}
|
||||||
// Ensure the wait counter is decremented if we exit before acquiring the slot.
|
releaseWait := func() {
|
||||||
defer func() {
|
|
||||||
if accountWaitCounted {
|
if accountWaitCounted {
|
||||||
h.concurrencyHelper.DecrementAccountWaitCount(c.Request.Context(), account.ID)
|
h.concurrencyHelper.DecrementAccountWaitCount(c.Request.Context(), account.ID)
|
||||||
|
accountWaitCounted = false
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
|
||||||
accountReleaseFunc, err = h.concurrencyHelper.AcquireAccountSlotWithWaitTimeout(
|
accountReleaseFunc, err = h.concurrencyHelper.AcquireAccountSlotWithWaitTimeout(
|
||||||
c,
|
c,
|
||||||
@@ -326,17 +346,15 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
&streamStarted,
|
&streamStarted,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Account concurrency acquire failed: %v", err)
|
reqLog.Warn("gateway.account_slot_acquire_failed", zap.Int64("account_id", account.ID), zap.Error(err))
|
||||||
|
releaseWait()
|
||||||
h.handleConcurrencyError(c, err, "account", streamStarted)
|
h.handleConcurrencyError(c, err, "account", streamStarted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Slot acquired: no longer waiting in queue.
|
// Slot acquired: no longer waiting in queue.
|
||||||
if accountWaitCounted {
|
releaseWait()
|
||||||
h.concurrencyHelper.DecrementAccountWaitCount(c.Request.Context(), account.ID)
|
|
||||||
accountWaitCounted = false
|
|
||||||
}
|
|
||||||
if err := h.gatewayService.BindStickySession(c.Request.Context(), apiKey.GroupID, sessionKey, account.ID); err != nil {
|
if err := h.gatewayService.BindStickySession(c.Request.Context(), apiKey.GroupID, sessionKey, account.ID); err != nil {
|
||||||
log.Printf("Bind sticky session failed: %v", err)
|
reqLog.Warn("gateway.bind_sticky_session_failed", zap.Int64("account_id", account.ID), zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 账号槽位/等待计数需要在超时或断开时安全回收
|
// 账号槽位/等待计数需要在超时或断开时安全回收
|
||||||
@@ -345,8 +363,8 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
// 转发请求 - 根据账号平台分流
|
// 转发请求 - 根据账号平台分流
|
||||||
var result *service.ForwardResult
|
var result *service.ForwardResult
|
||||||
requestCtx := c.Request.Context()
|
requestCtx := c.Request.Context()
|
||||||
if switchCount > 0 {
|
if fs.SwitchCount > 0 {
|
||||||
requestCtx = context.WithValue(requestCtx, ctxkey.AccountSwitchCount, switchCount)
|
requestCtx = context.WithValue(requestCtx, ctxkey.AccountSwitchCount, fs.SwitchCount)
|
||||||
}
|
}
|
||||||
if account.Platform == service.PlatformAntigravity {
|
if account.Platform == service.PlatformAntigravity {
|
||||||
result, err = h.antigravityGatewayService.ForwardGemini(requestCtx, c, account, reqModel, "generateContent", reqStream, body, hasBoundSession)
|
result, err = h.antigravityGatewayService.ForwardGemini(requestCtx, c, account, reqModel, "generateContent", reqStream, body, hasBoundSession)
|
||||||
@@ -359,26 +377,23 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
var failoverErr *service.UpstreamFailoverError
|
var failoverErr *service.UpstreamFailoverError
|
||||||
if errors.As(err, &failoverErr) {
|
if errors.As(err, &failoverErr) {
|
||||||
failedAccountIDs[account.ID] = struct{}{}
|
action := fs.HandleFailoverError(c.Request.Context(), h.gatewayService, account.ID, account.Platform, failoverErr)
|
||||||
lastFailoverErr = failoverErr
|
switch action {
|
||||||
if needForceCacheBilling(hasBoundSession, failoverErr) {
|
case FailoverContinue:
|
||||||
forceCacheBilling = true
|
continue
|
||||||
}
|
case FailoverExhausted:
|
||||||
if switchCount >= maxAccountSwitches {
|
h.handleFailoverExhausted(c, fs.LastFailoverErr, service.PlatformGemini, streamStarted)
|
||||||
h.handleFailoverExhausted(c, failoverErr, service.PlatformGemini, streamStarted)
|
return
|
||||||
|
case FailoverCanceled:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switchCount++
|
|
||||||
log.Printf("Account %d: upstream error %d, switching account %d/%d", account.ID, failoverErr.StatusCode, switchCount, maxAccountSwitches)
|
|
||||||
if account.Platform == service.PlatformAntigravity {
|
|
||||||
if !sleepFailoverDelay(c.Request.Context(), switchCount) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
// 错误响应已在Forward中处理,这里只记录日志
|
wroteFallback := h.ensureForwardErrorResponse(c, streamStarted)
|
||||||
log.Printf("Forward request failed: %v", err)
|
reqLog.Error("gateway.forward_failed",
|
||||||
|
zap.Int64("account_id", account.ID),
|
||||||
|
zap.Bool("fallback_error_response_written", wroteFallback),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,24 +401,29 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
userAgent := c.GetHeader("User-Agent")
|
userAgent := c.GetHeader("User-Agent")
|
||||||
clientIP := ip.GetClientIP(c)
|
clientIP := ip.GetClientIP(c)
|
||||||
|
|
||||||
// 异步记录使用量(subscription已在函数开头获取)
|
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
|
||||||
go func(result *service.ForwardResult, usedAccount *service.Account, ua, clientIP string, fcb bool) {
|
h.submitUsageRecordTask(func(ctx context.Context) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
||||||
Result: result,
|
Result: result,
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
User: apiKey.User,
|
User: apiKey.User,
|
||||||
Account: usedAccount,
|
Account: account,
|
||||||
Subscription: subscription,
|
Subscription: subscription,
|
||||||
UserAgent: ua,
|
UserAgent: userAgent,
|
||||||
IPAddress: clientIP,
|
IPAddress: clientIP,
|
||||||
ForceCacheBilling: fcb,
|
ForceCacheBilling: fs.ForceCacheBilling,
|
||||||
APIKeyService: h.apiKeyService,
|
APIKeyService: h.apiKeyService,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("Record usage failed: %v", err)
|
logger.L().With(
|
||||||
|
zap.String("component", "handler.gateway.messages"),
|
||||||
|
zap.Int64("user_id", subject.UserID),
|
||||||
|
zap.Int64("api_key_id", apiKey.ID),
|
||||||
|
zap.Any("group_id", apiKey.GroupID),
|
||||||
|
zap.String("model", reqModel),
|
||||||
|
zap.Int64("account_id", account.ID),
|
||||||
|
).Error("gateway.record_usage_failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
}(result, account, userAgent, clientIP, forceCacheBilling)
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,43 +444,36 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
maxAccountSwitches := h.maxAccountSwitches
|
fs := NewFailoverState(h.maxAccountSwitches, hasBoundSession)
|
||||||
switchCount := 0
|
|
||||||
failedAccountIDs := make(map[int64]struct{})
|
|
||||||
var lastFailoverErr *service.UpstreamFailoverError
|
|
||||||
retryWithFallback := false
|
retryWithFallback := false
|
||||||
var forceCacheBilling bool // 粘性会话切换时的缓存计费标记
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// 选择支持该模型的账号
|
// 选择支持该模型的账号
|
||||||
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), currentAPIKey.GroupID, sessionKey, reqModel, failedAccountIDs, parsedReq.MetadataUserID)
|
selection, err := h.gatewayService.SelectAccountWithLoadAwareness(c.Request.Context(), currentAPIKey.GroupID, sessionKey, reqModel, fs.FailedAccountIDs, parsedReq.MetadataUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(failedAccountIDs) == 0 {
|
if len(fs.FailedAccountIDs) == 0 {
|
||||||
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted)
|
h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Antigravity 单账号退避重试:分组内没有其他可用账号时,
|
action := fs.HandleSelectionExhausted(c.Request.Context())
|
||||||
// 对 503 错误不直接返回,而是清除排除列表、等待退避后重试同一个账号。
|
switch action {
|
||||||
// 谷歌上游 503 (MODEL_CAPACITY_EXHAUSTED) 通常是暂时性的,等几秒就能恢复。
|
case FailoverContinue:
|
||||||
if lastFailoverErr != nil && lastFailoverErr.StatusCode == http.StatusServiceUnavailable && switchCount <= maxAccountSwitches {
|
ctx := context.WithValue(c.Request.Context(), ctxkey.SingleAccountRetry, true)
|
||||||
if sleepAntigravitySingleAccountBackoff(c.Request.Context(), switchCount) {
|
c.Request = c.Request.WithContext(ctx)
|
||||||
log.Printf("Antigravity single-account 503 retry: clearing failed accounts, retry %d/%d", switchCount, maxAccountSwitches)
|
continue
|
||||||
failedAccountIDs = make(map[int64]struct{})
|
case FailoverCanceled:
|
||||||
// 设置 context 标记,让 Service 层预检查等待限流过期而非直接切换
|
return
|
||||||
ctx := context.WithValue(c.Request.Context(), ctxkey.SingleAccountRetry, true)
|
default: // FailoverExhausted
|
||||||
c.Request = c.Request.WithContext(ctx)
|
if fs.LastFailoverErr != nil {
|
||||||
continue
|
h.handleFailoverExhausted(c, fs.LastFailoverErr, platform, streamStarted)
|
||||||
|
} else {
|
||||||
|
h.handleFailoverExhaustedSimple(c, 502, streamStarted)
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if lastFailoverErr != nil {
|
|
||||||
h.handleFailoverExhausted(c, lastFailoverErr, platform, streamStarted)
|
|
||||||
} else {
|
|
||||||
h.handleFailoverExhaustedSimple(c, 502, streamStarted)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
account := selection.Account
|
account := selection.Account
|
||||||
setOpsSelectedAccount(c, account.ID)
|
setOpsSelectedAccount(c, account.ID, account.Platform)
|
||||||
|
|
||||||
// 检查请求拦截(预热请求、SUGGESTION MODE等)
|
// 检查请求拦截(预热请求、SUGGESTION MODE等)
|
||||||
if account.IsInterceptWarmupEnabled() {
|
if account.IsInterceptWarmupEnabled() {
|
||||||
@@ -488,20 +501,24 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
accountWaitCounted := false
|
accountWaitCounted := false
|
||||||
canWait, err := h.concurrencyHelper.IncrementAccountWaitCount(c.Request.Context(), account.ID, selection.WaitPlan.MaxWaiting)
|
canWait, err := h.concurrencyHelper.IncrementAccountWaitCount(c.Request.Context(), account.ID, selection.WaitPlan.MaxWaiting)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Increment account wait count failed: %v", err)
|
reqLog.Warn("gateway.account_wait_counter_increment_failed", zap.Int64("account_id", account.ID), zap.Error(err))
|
||||||
} else if !canWait {
|
} else if !canWait {
|
||||||
log.Printf("Account wait queue full: account=%d", account.ID)
|
reqLog.Info("gateway.account_wait_queue_full",
|
||||||
|
zap.Int64("account_id", account.ID),
|
||||||
|
zap.Int("max_waiting", selection.WaitPlan.MaxWaiting),
|
||||||
|
)
|
||||||
h.handleStreamingAwareError(c, http.StatusTooManyRequests, "rate_limit_error", "Too many pending requests, please retry later", streamStarted)
|
h.handleStreamingAwareError(c, http.StatusTooManyRequests, "rate_limit_error", "Too many pending requests, please retry later", streamStarted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err == nil && canWait {
|
if err == nil && canWait {
|
||||||
accountWaitCounted = true
|
accountWaitCounted = true
|
||||||
}
|
}
|
||||||
defer func() {
|
releaseWait := func() {
|
||||||
if accountWaitCounted {
|
if accountWaitCounted {
|
||||||
h.concurrencyHelper.DecrementAccountWaitCount(c.Request.Context(), account.ID)
|
h.concurrencyHelper.DecrementAccountWaitCount(c.Request.Context(), account.ID)
|
||||||
|
accountWaitCounted = false
|
||||||
}
|
}
|
||||||
}()
|
}
|
||||||
|
|
||||||
accountReleaseFunc, err = h.concurrencyHelper.AcquireAccountSlotWithWaitTimeout(
|
accountReleaseFunc, err = h.concurrencyHelper.AcquireAccountSlotWithWaitTimeout(
|
||||||
c,
|
c,
|
||||||
@@ -512,16 +529,15 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
&streamStarted,
|
&streamStarted,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Account concurrency acquire failed: %v", err)
|
reqLog.Warn("gateway.account_slot_acquire_failed", zap.Int64("account_id", account.ID), zap.Error(err))
|
||||||
|
releaseWait()
|
||||||
h.handleConcurrencyError(c, err, "account", streamStarted)
|
h.handleConcurrencyError(c, err, "account", streamStarted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if accountWaitCounted {
|
// Slot acquired: no longer waiting in queue.
|
||||||
h.concurrencyHelper.DecrementAccountWaitCount(c.Request.Context(), account.ID)
|
releaseWait()
|
||||||
accountWaitCounted = false
|
|
||||||
}
|
|
||||||
if err := h.gatewayService.BindStickySession(c.Request.Context(), currentAPIKey.GroupID, sessionKey, account.ID); err != nil {
|
if err := h.gatewayService.BindStickySession(c.Request.Context(), currentAPIKey.GroupID, sessionKey, account.ID); err != nil {
|
||||||
log.Printf("Bind sticky session failed: %v", err)
|
reqLog.Warn("gateway.bind_sticky_session_failed", zap.Int64("account_id", account.ID), zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 账号槽位/等待计数需要在超时或断开时安全回收
|
// 账号槽位/等待计数需要在超时或断开时安全回收
|
||||||
@@ -530,8 +546,8 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
// 转发请求 - 根据账号平台分流
|
// 转发请求 - 根据账号平台分流
|
||||||
var result *service.ForwardResult
|
var result *service.ForwardResult
|
||||||
requestCtx := c.Request.Context()
|
requestCtx := c.Request.Context()
|
||||||
if switchCount > 0 {
|
if fs.SwitchCount > 0 {
|
||||||
requestCtx = context.WithValue(requestCtx, ctxkey.AccountSwitchCount, switchCount)
|
requestCtx = context.WithValue(requestCtx, ctxkey.AccountSwitchCount, fs.SwitchCount)
|
||||||
}
|
}
|
||||||
if account.Platform == service.PlatformAntigravity && account.Type != service.AccountTypeAPIKey {
|
if account.Platform == service.PlatformAntigravity && account.Type != service.AccountTypeAPIKey {
|
||||||
result, err = h.antigravityGatewayService.Forward(requestCtx, c, account, body, hasBoundSession)
|
result, err = h.antigravityGatewayService.Forward(requestCtx, c, account, body, hasBoundSession)
|
||||||
@@ -544,18 +560,26 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
var promptTooLongErr *service.PromptTooLongError
|
var promptTooLongErr *service.PromptTooLongError
|
||||||
if errors.As(err, &promptTooLongErr) {
|
if errors.As(err, &promptTooLongErr) {
|
||||||
log.Printf("Prompt too long from antigravity: group=%d fallback_group_id=%v fallback_used=%v", currentAPIKey.GroupID, fallbackGroupID, fallbackUsed)
|
reqLog.Warn("gateway.prompt_too_long_from_antigravity",
|
||||||
|
zap.Any("current_group_id", currentAPIKey.GroupID),
|
||||||
|
zap.Any("fallback_group_id", fallbackGroupID),
|
||||||
|
zap.Bool("fallback_used", fallbackUsed),
|
||||||
|
)
|
||||||
if !fallbackUsed && fallbackGroupID != nil && *fallbackGroupID > 0 {
|
if !fallbackUsed && fallbackGroupID != nil && *fallbackGroupID > 0 {
|
||||||
fallbackGroup, err := h.gatewayService.ResolveGroupByID(c.Request.Context(), *fallbackGroupID)
|
fallbackGroup, err := h.gatewayService.ResolveGroupByID(c.Request.Context(), *fallbackGroupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Resolve fallback group failed: %v", err)
|
reqLog.Warn("gateway.resolve_fallback_group_failed", zap.Int64("fallback_group_id", *fallbackGroupID), zap.Error(err))
|
||||||
_ = h.antigravityGatewayService.WriteMappedClaudeError(c, account, promptTooLongErr.StatusCode, promptTooLongErr.RequestID, promptTooLongErr.Body)
|
_ = h.antigravityGatewayService.WriteMappedClaudeError(c, account, promptTooLongErr.StatusCode, promptTooLongErr.RequestID, promptTooLongErr.Body)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if fallbackGroup.Platform != service.PlatformAnthropic ||
|
if fallbackGroup.Platform != service.PlatformAnthropic ||
|
||||||
fallbackGroup.SubscriptionType == service.SubscriptionTypeSubscription ||
|
fallbackGroup.SubscriptionType == service.SubscriptionTypeSubscription ||
|
||||||
fallbackGroup.FallbackGroupIDOnInvalidRequest != nil {
|
fallbackGroup.FallbackGroupIDOnInvalidRequest != nil {
|
||||||
log.Printf("Fallback group invalid: group=%d platform=%s subscription=%s", fallbackGroup.ID, fallbackGroup.Platform, fallbackGroup.SubscriptionType)
|
reqLog.Warn("gateway.fallback_group_invalid",
|
||||||
|
zap.Int64("fallback_group_id", fallbackGroup.ID),
|
||||||
|
zap.String("fallback_platform", fallbackGroup.Platform),
|
||||||
|
zap.String("fallback_subscription_type", fallbackGroup.SubscriptionType),
|
||||||
|
)
|
||||||
_ = h.antigravityGatewayService.WriteMappedClaudeError(c, account, promptTooLongErr.StatusCode, promptTooLongErr.RequestID, promptTooLongErr.Body)
|
_ = h.antigravityGatewayService.WriteMappedClaudeError(c, account, promptTooLongErr.StatusCode, promptTooLongErr.RequestID, promptTooLongErr.Body)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -579,26 +603,23 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
var failoverErr *service.UpstreamFailoverError
|
var failoverErr *service.UpstreamFailoverError
|
||||||
if errors.As(err, &failoverErr) {
|
if errors.As(err, &failoverErr) {
|
||||||
failedAccountIDs[account.ID] = struct{}{}
|
action := fs.HandleFailoverError(c.Request.Context(), h.gatewayService, account.ID, account.Platform, failoverErr)
|
||||||
lastFailoverErr = failoverErr
|
switch action {
|
||||||
if needForceCacheBilling(hasBoundSession, failoverErr) {
|
case FailoverContinue:
|
||||||
forceCacheBilling = true
|
continue
|
||||||
}
|
case FailoverExhausted:
|
||||||
if switchCount >= maxAccountSwitches {
|
h.handleFailoverExhausted(c, fs.LastFailoverErr, account.Platform, streamStarted)
|
||||||
h.handleFailoverExhausted(c, failoverErr, account.Platform, streamStarted)
|
return
|
||||||
|
case FailoverCanceled:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switchCount++
|
|
||||||
log.Printf("Account %d: upstream error %d, switching account %d/%d", account.ID, failoverErr.StatusCode, switchCount, maxAccountSwitches)
|
|
||||||
if account.Platform == service.PlatformAntigravity {
|
|
||||||
if !sleepFailoverDelay(c.Request.Context(), switchCount) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
// 错误响应已在Forward中处理,这里只记录日志
|
wroteFallback := h.ensureForwardErrorResponse(c, streamStarted)
|
||||||
log.Printf("Account %d: Forward request failed: %v", account.ID, err)
|
reqLog.Error("gateway.forward_failed",
|
||||||
|
zap.Int64("account_id", account.ID),
|
||||||
|
zap.Bool("fallback_error_response_written", wroteFallback),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,24 +627,29 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
userAgent := c.GetHeader("User-Agent")
|
userAgent := c.GetHeader("User-Agent")
|
||||||
clientIP := ip.GetClientIP(c)
|
clientIP := ip.GetClientIP(c)
|
||||||
|
|
||||||
// 异步记录使用量(subscription已在函数开头获取)
|
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
|
||||||
go func(result *service.ForwardResult, usedAccount *service.Account, ua, clientIP string, fcb bool) {
|
h.submitUsageRecordTask(func(ctx context.Context) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{
|
||||||
Result: result,
|
Result: result,
|
||||||
APIKey: currentAPIKey,
|
APIKey: currentAPIKey,
|
||||||
User: currentAPIKey.User,
|
User: currentAPIKey.User,
|
||||||
Account: usedAccount,
|
Account: account,
|
||||||
Subscription: currentSubscription,
|
Subscription: currentSubscription,
|
||||||
UserAgent: ua,
|
UserAgent: userAgent,
|
||||||
IPAddress: clientIP,
|
IPAddress: clientIP,
|
||||||
ForceCacheBilling: fcb,
|
ForceCacheBilling: fs.ForceCacheBilling,
|
||||||
APIKeyService: h.apiKeyService,
|
APIKeyService: h.apiKeyService,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("Record usage failed: %v", err)
|
logger.L().With(
|
||||||
|
zap.String("component", "handler.gateway.messages"),
|
||||||
|
zap.Int64("user_id", subject.UserID),
|
||||||
|
zap.Int64("api_key_id", currentAPIKey.ID),
|
||||||
|
zap.Any("group_id", currentAPIKey.GroupID),
|
||||||
|
zap.String("model", reqModel),
|
||||||
|
zap.Int64("account_id", account.ID),
|
||||||
|
).Error("gateway.record_usage_failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
}(result, account, userAgent, clientIP, forceCacheBilling)
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !retryWithFallback {
|
if !retryWithFallback {
|
||||||
@@ -646,6 +672,17 @@ func (h *GatewayHandler) Models(c *gin.Context) {
|
|||||||
groupID = &apiKey.Group.ID
|
groupID = &apiKey.Group.ID
|
||||||
platform = apiKey.Group.Platform
|
platform = apiKey.Group.Platform
|
||||||
}
|
}
|
||||||
|
if forcedPlatform, ok := middleware2.GetForcePlatformFromContext(c); ok && strings.TrimSpace(forcedPlatform) != "" {
|
||||||
|
platform = forcedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
if platform == service.PlatformSora {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"object": "list",
|
||||||
|
"data": service.DefaultSoraModels(h.cfg),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get available models from account configurations (without platform filter)
|
// Get available models from account configurations (without platform filter)
|
||||||
availableModels := h.gatewayService.GetAvailableModels(c.Request.Context(), groupID, "")
|
availableModels := h.gatewayService.GetAvailableModels(c.Request.Context(), groupID, "")
|
||||||
@@ -857,48 +894,6 @@ func (h *GatewayHandler) handleConcurrencyError(c *gin.Context, err error, slotT
|
|||||||
fmt.Sprintf("Concurrency limit exceeded for %s, please retry later", slotType), streamStarted)
|
fmt.Sprintf("Concurrency limit exceeded for %s, please retry later", slotType), streamStarted)
|
||||||
}
|
}
|
||||||
|
|
||||||
// needForceCacheBilling 判断 failover 时是否需要强制缓存计费
|
|
||||||
// 粘性会话切换账号、或上游明确标记时,将 input_tokens 转为 cache_read 计费
|
|
||||||
func needForceCacheBilling(hasBoundSession bool, failoverErr *service.UpstreamFailoverError) bool {
|
|
||||||
return hasBoundSession || (failoverErr != nil && failoverErr.ForceCacheBilling)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sleepFailoverDelay 账号切换线性递增延时:第1次0s、第2次1s、第3次2s…
|
|
||||||
// 返回 false 表示 context 已取消。
|
|
||||||
func sleepFailoverDelay(ctx context.Context, switchCount int) bool {
|
|
||||||
delay := time.Duration(switchCount-1) * time.Second
|
|
||||||
if delay <= 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false
|
|
||||||
case <-time.After(delay):
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sleepAntigravitySingleAccountBackoff Antigravity 平台单账号分组的 503 退避重试延时。
|
|
||||||
// 当分组内只有一个可用账号且上游返回 503(MODEL_CAPACITY_EXHAUSTED)时使用,
|
|
||||||
// 采用短固定延时策略。Service 层在 SingleAccountRetry 模式下已经做了充分的原地重试
|
|
||||||
// (最多 3 次、总等待 30s),所以 Handler 层的退避只需短暂等待即可。
|
|
||||||
// 返回 false 表示 context 已取消。
|
|
||||||
func sleepAntigravitySingleAccountBackoff(ctx context.Context, retryCount int) bool {
|
|
||||||
// 固定短延时:2s
|
|
||||||
// Service 层已经在原地等待了足够长的时间(retryDelay × 重试次数),
|
|
||||||
// Handler 层只需短暂间隔后重新进入 Service 层即可。
|
|
||||||
const delay = 2 * time.Second
|
|
||||||
|
|
||||||
log.Printf("Antigravity single-account 503 backoff: waiting %v before retry (attempt %d)", delay, retryCount)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return false
|
|
||||||
case <-time.After(delay):
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *GatewayHandler) handleFailoverExhausted(c *gin.Context, failoverErr *service.UpstreamFailoverError, platform string, streamStarted bool) {
|
func (h *GatewayHandler) handleFailoverExhausted(c *gin.Context, failoverErr *service.UpstreamFailoverError, platform string, streamStarted bool) {
|
||||||
statusCode := failoverErr.StatusCode
|
statusCode := failoverErr.StatusCode
|
||||||
responseBody := failoverErr.ResponseBody
|
responseBody := failoverErr.ResponseBody
|
||||||
@@ -918,6 +913,10 @@ func (h *GatewayHandler) handleFailoverExhausted(c *gin.Context, failoverErr *se
|
|||||||
msg = *rule.CustomMessage
|
msg = *rule.CustomMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rule.SkipMonitoring {
|
||||||
|
c.Set(service.OpsSkipPassthroughKey, true)
|
||||||
|
}
|
||||||
|
|
||||||
h.handleStreamingAwareError(c, respCode, "upstream_error", msg, streamStarted)
|
h.handleStreamingAwareError(c, respCode, "upstream_error", msg, streamStarted)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -983,6 +982,15 @@ func (h *GatewayHandler) handleStreamingAwareError(c *gin.Context, status int, e
|
|||||||
h.errorResponse(c, status, errType, message)
|
h.errorResponse(c, status, errType, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureForwardErrorResponse 在 Forward 返回错误但尚未写响应时补写统一错误响应。
|
||||||
|
func (h *GatewayHandler) ensureForwardErrorResponse(c *gin.Context, streamStarted bool) bool {
|
||||||
|
if c == nil || c.Writer == nil || c.Writer.Written() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
h.handleStreamingAwareError(c, http.StatusBadGateway, "upstream_error", "Upstream request failed", streamStarted)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// errorResponse 返回Claude API格式的错误响应
|
// errorResponse 返回Claude API格式的错误响应
|
||||||
func (h *GatewayHandler) errorResponse(c *gin.Context, status int, errType, message string) {
|
func (h *GatewayHandler) errorResponse(c *gin.Context, status int, errType, message string) {
|
||||||
c.JSON(status, gin.H{
|
c.JSON(status, gin.H{
|
||||||
@@ -1010,6 +1018,12 @@ func (h *GatewayHandler) CountTokens(c *gin.Context) {
|
|||||||
h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
|
h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
reqLog := requestLogger(
|
||||||
|
c,
|
||||||
|
"handler.gateway.count_tokens",
|
||||||
|
zap.Int64("api_key_id", apiKey.ID),
|
||||||
|
zap.Any("group_id", apiKey.GroupID),
|
||||||
|
)
|
||||||
|
|
||||||
// 读取请求体
|
// 读取请求体
|
||||||
body, err := io.ReadAll(c.Request.Body)
|
body, err := io.ReadAll(c.Request.Body)
|
||||||
@@ -1037,6 +1051,7 @@ func (h *GatewayHandler) CountTokens(c *gin.Context) {
|
|||||||
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
|
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
reqLog = reqLog.With(zap.String("model", parsedReq.Model), zap.Bool("stream", parsedReq.Stream))
|
||||||
// 在请求上下文中记录 thinking 状态,供 Antigravity 最终模型 key 推导/模型维度限流使用
|
// 在请求上下文中记录 thinking 状态,供 Antigravity 最终模型 key 推导/模型维度限流使用
|
||||||
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), ctxkey.ThinkingEnabled, parsedReq.ThinkingEnabled))
|
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), ctxkey.ThinkingEnabled, parsedReq.ThinkingEnabled))
|
||||||
|
|
||||||
@@ -1070,14 +1085,15 @@ func (h *GatewayHandler) CountTokens(c *gin.Context) {
|
|||||||
// 选择支持该模型的账号
|
// 选择支持该模型的账号
|
||||||
account, err := h.gatewayService.SelectAccountForModel(c.Request.Context(), apiKey.GroupID, sessionHash, parsedReq.Model)
|
account, err := h.gatewayService.SelectAccountForModel(c.Request.Context(), apiKey.GroupID, sessionHash, parsedReq.Model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.errorResponse(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error())
|
reqLog.Warn("gateway.count_tokens_select_account_failed", zap.Error(err))
|
||||||
|
h.errorResponse(c, http.StatusServiceUnavailable, "api_error", "Service temporarily unavailable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setOpsSelectedAccount(c, account.ID)
|
setOpsSelectedAccount(c, account.ID, account.Platform)
|
||||||
|
|
||||||
// 转发请求(不记录使用量)
|
// 转发请求(不记录使用量)
|
||||||
if err := h.gatewayService.ForwardCountTokens(c.Request.Context(), c, account, parsedReq); err != nil {
|
if err := h.gatewayService.ForwardCountTokens(c.Request.Context(), c, account, parsedReq); err != nil {
|
||||||
log.Printf("Forward count_tokens request failed: %v", err)
|
reqLog.Error("gateway.count_tokens_forward_failed", zap.Int64("account_id", account.ID), zap.Error(err))
|
||||||
// 错误响应已在 ForwardCountTokens 中处理
|
// 错误响应已在 ForwardCountTokens 中处理
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1341,7 +1357,25 @@ func billingErrorDetails(err error) (status int, code, message string) {
|
|||||||
}
|
}
|
||||||
msg := pkgerrors.Message(err)
|
msg := pkgerrors.Message(err)
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
msg = err.Error()
|
logger.L().With(
|
||||||
|
zap.String("component", "handler.gateway.billing"),
|
||||||
|
zap.Error(err),
|
||||||
|
).Warn("gateway.billing_error_missing_message")
|
||||||
|
msg = "Billing error"
|
||||||
}
|
}
|
||||||
return http.StatusForbidden, "billing_error", msg
|
return http.StatusForbidden, "billing_error", msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *GatewayHandler) submitUsageRecordTask(task service.UsageRecordTask) {
|
||||||
|
if task == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.usageRecordWorkerPool != nil {
|
||||||
|
h.usageRecordWorkerPool.Submit(task)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 回退路径:worker 池未注入时同步执行,避免退回到无界 goroutine 模式。
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
task(ctx)
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user