Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
3d79773b
Commit
3d79773b
authored
Mar 04, 2026
by
kyx236
Browse files
Merge branch 'main' of
https://github.com/james-6-23/sub2api
parents
6aa8cbbf
742e73c9
Changes
738
Expand all
Hide whitespace changes
Inline
Side-by-side
.github/workflows/backend-ci.yml
View file @
3d79773b
...
...
@@ -11,8 +11,8 @@ jobs:
test
:
runs-on
:
ubuntu-latest
steps
:
-
uses
:
actions/checkout@v
4
-
uses
:
actions/setup-go@v
5
-
uses
:
actions/checkout@v
6
-
uses
:
actions/setup-go@v
6
with
:
go-version-file
:
backend/go.mod
check-latest
:
false
...
...
@@ -30,8 +30,8 @@ jobs:
golangci-lint
:
runs-on
:
ubuntu-latest
steps
:
-
uses
:
actions/checkout@v
4
-
uses
:
actions/setup-go@v
5
-
uses
:
actions/checkout@v
6
-
uses
:
actions/setup-go@v
6
with
:
go-version-file
:
backend/go.mod
check-latest
:
false
...
...
@@ -43,5 +43,5 @@ jobs:
uses
:
golangci/golangci-lint-action@v9
with
:
version
:
v2.7
args
:
--timeout=
5
m
working-directory
:
backend
args
:
--timeout=
30
m
working-directory
:
backend
\ No newline at end of file
.github/workflows/release.yml
View file @
3d79773b
...
...
@@ -31,7 +31,7 @@ jobs:
runs-on
:
ubuntu-latest
steps
:
-
name
:
Checkout
uses
:
actions/checkout@v
4
uses
:
actions/checkout@v
6
-
name
:
Update VERSION file
run
:
|
...
...
@@ -45,7 +45,7 @@ jobs:
echo "Updated VERSION file to: $VERSION"
-
name
:
Upload VERSION artifact
uses
:
actions/upload-artifact@v
4
uses
:
actions/upload-artifact@v
7
with
:
name
:
version-file
path
:
backend/cmd/server/VERSION
...
...
@@ -55,7 +55,7 @@ jobs:
runs-on
:
ubuntu-latest
steps
:
-
name
:
Checkout
uses
:
actions/checkout@v
4
uses
:
actions/checkout@v
6
-
name
:
Setup pnpm
uses
:
pnpm/action-setup@v4
...
...
@@ -63,7 +63,7 @@ jobs:
version
:
9
-
name
:
Setup Node.js
uses
:
actions/setup-node@v
4
uses
:
actions/setup-node@v
6
with
:
node-version
:
'
20'
cache
:
'
pnpm'
...
...
@@ -78,7 +78,7 @@ jobs:
working-directory
:
frontend
-
name
:
Upload frontend artifact
uses
:
actions/upload-artifact@v
4
uses
:
actions/upload-artifact@v
7
with
:
name
:
frontend-dist
path
:
backend/internal/web/dist/
...
...
@@ -89,25 +89,25 @@ jobs:
runs-on
:
ubuntu-latest
steps
:
-
name
:
Checkout
uses
:
actions/checkout@v
4
uses
:
actions/checkout@v
6
with
:
fetch-depth
:
0
ref
:
${{ github.event.inputs.tag || github.ref }}
-
name
:
Download VERSION artifact
uses
:
actions/download-artifact@v
4
uses
:
actions/download-artifact@v
8
with
:
name
:
version-file
path
:
backend/cmd/server/
-
name
:
Download frontend artifact
uses
:
actions/download-artifact@v
4
uses
:
actions/download-artifact@v
8
with
:
name
:
frontend-dist
path
:
backend/internal/web/dist/
-
name
:
Setup Go
uses
:
actions/setup-go@v
5
uses
:
actions/setup-go@v
6
with
:
go-version-file
:
backend/go.mod
check-latest
:
false
...
...
@@ -173,7 +173,7 @@ jobs:
run
:
echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
-
name
:
Run GoReleaser
uses
:
goreleaser/goreleaser-action@v
6
uses
:
goreleaser/goreleaser-action@v
7
with
:
version
:
'
~>
v2'
args
:
release --clean --skip=validate ${{ env.SIMPLE_RELEASE == 'true' && '--config=.goreleaser.simple.yaml' || '' }}
...
...
@@ -188,7 +188,7 @@ jobs:
# Update DockerHub description
-
name
:
Update DockerHub description
if
:
${{ env.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }}
uses
:
peter-evans/dockerhub-description@v
4
uses
:
peter-evans/dockerhub-description@v
5
env
:
DOCKERHUB_USERNAME
:
${{ secrets.DOCKERHUB_USERNAME }}
with
:
...
...
.github/workflows/security-scan.yml
View file @
3d79773b
...
...
@@ -12,10 +12,11 @@ permissions:
jobs
:
backend-security
:
runs-on
:
ubuntu-latest
timeout-minutes
:
15
steps
:
-
uses
:
actions/checkout@v
4
-
uses
:
actions/checkout@v
6
-
name
:
Set up Go
uses
:
actions/setup-go@v
5
uses
:
actions/setup-go@v
6
with
:
go-version-file
:
backend/go.mod
check-latest
:
false
...
...
@@ -28,22 +29,17 @@ jobs:
run
:
|
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
-
name
:
Run gosec
working-directory
:
backend
run
:
|
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec -severity high -confidence high ./...
frontend-security
:
runs-on
:
ubuntu-latest
steps
:
-
uses
:
actions/checkout@v
4
-
uses
:
actions/checkout@v
6
-
name
:
Set up pnpm
uses
:
pnpm/action-setup@v4
with
:
version
:
9
-
name
:
Set up Node.js
uses
:
actions/setup-node@v
4
uses
:
actions/setup-node@v
6
with
:
node-version
:
'
20'
cache
:
'
pnpm'
...
...
.gitignore
View file @
3d79773b
...
...
@@ -116,17 +116,20 @@ backend/.installed
# ===================
tests
CLAUDE.md
AGENTS.md
.claude
scripts
.code-review-state
openspec/
docs/
#openspec/
code-reviews/
AGENTS.md
#
AGENTS.md
backend/cmd/server/server
deploy/docker-compose.override.yml
.gocache/
vite.config.js
docs/*
.serena/
\ No newline at end of file
.serena/
.codex/
frontend/coverage/
aicodex
output/
DEV_GUIDE.md
View file @
3d79773b
...
...
@@ -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 前务必本地验证:
...
...
Dockerfile
View file @
3d79773b
...
...
@@ -8,7 +8,7 @@
ARG
NODE_IMAGE=node:24-alpine
ARG
GOLANG_IMAGE=golang:1.25.7-alpine
ARG
ALPINE_IMAGE=alpine:3.2
0
ARG
ALPINE_IMAGE=alpine:3.2
1
ARG
GOPROXY=https://goproxy.cn,direct
ARG
GOSUMDB=sum.golang.google.cn
...
...
@@ -36,7 +36,7 @@ RUN pnpm run build
FROM
${GOLANG_IMAGE} AS backend-builder
# Build arguments for version info (set by CI)
ARG
VERSION=
docker
ARG
VERSION=
ARG
COMMIT=docker
ARG
DATE
ARG
GOPROXY
...
...
@@ -61,9 +61,14 @@ COPY backend/ ./
COPY
--from=frontend-builder /app/backend/internal/web/dist ./internal/web/dist
# 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
\
-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"
\
-trimpath
\
-o
/app/sub2api
\
./cmd/server
...
...
@@ -81,7 +86,6 @@ LABEL org.opencontainers.image.source="https://github.com/Wei-Shaw/sub2api"
RUN
apk add
--no-cache
\
ca-certificates
\
tzdata
\
curl
\
&&
rm
-rf
/var/cache/apk/
*
# Create non-root user
...
...
@@ -91,11 +95,12 @@ RUN addgroup -g 1000 sub2api && \
# Set working directory
WORKDIR
/app
# Copy binary from builder
COPY
--from=backend-builder /app/sub2api /app/sub2api
# Copy binary/resources with ownership to avoid extra full-layer chown copy
COPY
--from=backend-builder --chown=sub2api:sub2api /app/sub2api /app/sub2api
COPY
--from=backend-builder --chown=sub2api:sub2api /app/backend/resources /app/resources
# Create data directory
RUN
mkdir
-p
/app/data
&&
chown
-R
sub2api:sub2api /app
RUN
mkdir
-p
/app/data
&&
chown
sub2api:sub2api /app
/data
# Switch to non-root user
USER
sub2api
...
...
@@ -105,7 +110,7 @@ EXPOSE 8080
# Health check
HEALTHCHECK
--interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD
curl -f
http://localhost:${SERVER_PORT:-8080}/health || exit 1
CMD
wget -q -T 5 -O /dev/null
http://localhost:${SERVER_PORT:-8080}/health || exit 1
# Run the application
ENTRYPOINT
["/app/sub2api"]
Makefile
View file @
3d79773b
.PHONY
:
build build-backend build-frontend test test-backend test-frontend
.PHONY
:
build build-backend build-frontend
build-datamanagementd
test test-backend test-frontend
test-datamanagementd secret-scan
# 一键编译前后端
build
:
build-backend build-frontend
...
...
@@ -11,6 +11,10 @@ build-backend:
build-frontend
:
@
pnpm
--dir
frontend run build
# 编译 datamanagementd(宿主机数据管理进程)
build-datamanagementd
:
@
cd
datamanagement
&&
go build
-o
datamanagementd ./cmd/datamanagementd
# 运行测试(后端 + 前端)
test
:
test-backend test-frontend
...
...
@@ -20,3 +24,9 @@ test-backend:
test-frontend
:
@
pnpm
--dir
frontend run lint:check
@
pnpm
--dir
frontend run typecheck
test-datamanagementd
:
@
cd
datamanagement
&&
go
test
./...
secret-scan
:
@
python3 tools/secret_scan.py
README.md
View file @
3d79773b
...
...
@@ -54,6 +54,7 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot
## Documentation
-
Dependency Security:
`docs/dependency-security.md`
-
Admin Payment Integration API:
`docs/ADMIN_PAYMENT_INTEGRATION_API.md`
---
...
...
@@ -363,6 +364,12 @@ default:
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`
:
-
`cors.allowed_origins`
for CORS allowlist
...
...
README_CN.md
View file @
3d79773b
...
...
@@ -62,8 +62,6 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅(
-
当请求包含
`function_call_output`
时,需要携带
`previous_response_id`
,或在
`input`
中包含带
`call_id`
的
`tool_call`
/
`function_call`
,或带非空
`id`
且与
`function_call_output.call_id`
匹配的
`item_reference`
。
-
若依赖上游历史记录,网关会强制
`store=true`
并需要复用
`previous_response_id`
,以避免出现 “No tool call found for function call output” 错误。
---
## 部署方式
### 方式一:脚本安装(推荐)
...
...
@@ -244,6 +242,18 @@ docker-compose -f docker-compose.local.yml logs -f sub2api
**推荐:**
使用
`docker-compose.local.yml`
(脚本部署)以便更轻松地管理数据。
#### 启用“数据管理”功能(datamanagementd)
如需启用管理后台“数据管理”,需要额外部署宿主机数据管理进程
`datamanagementd`
。
关键点:
-
主进程固定探测:
`/tmp/sub2api-datamanagement.sock`
-
只有该 Socket 可连通时,数据管理功能才会开启
-
Docker 场景需将宿主机 Socket 挂载到容器同路径
详细部署步骤见:
`deploy/DATAMANAGEMENTD_CN.md`
#### 访问
在浏览器中打开
`http://你的服务器IP:8080`
...
...
@@ -370,6 +380,33 @@ default:
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`
还支持以下安全相关配置:
-
`cors.allowed_origins`
配置 CORS 白名单
...
...
@@ -383,6 +420,14 @@ default:
-
`server.trusted_proxies`
启用可信代理解析 X-Forwarded-For
-
`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 配置**
当
`security.url_allowlist.enabled=false`
时,系统默认执行最小 URL 校验,
**拒绝 HTTP URL**
,仅允许 HTTPS。要允许 HTTP URL(例如用于开发或内网测试),必须显式设置:
...
...
@@ -428,6 +473,29 @@ Invalid base URL: invalid url scheme: http
./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
...
...
backend/.golangci.yml
View file @
3d79773b
...
...
@@ -5,6 +5,7 @@ linters:
enable
:
-
depguard
-
errcheck
-
gosec
-
govet
-
ineffassign
-
staticcheck
...
...
@@ -42,6 +43,22 @@ linters:
desc
:
"
handler
must
not
import
gorm"
-
pkg
:
github.com/redis/go-redis/v9
desc
:
"
handler
must
not
import
redis"
gosec
:
excludes
:
-
G101
-
G103
-
G104
-
G109
-
G115
-
G201
-
G202
-
G301
-
G302
-
G304
-
G306
-
G404
severity
:
high
confidence
:
high
errcheck
:
# Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
# Such cases aren't reported by default.
...
...
backend/Makefile
View file @
3d79773b
.PHONY
:
build test test-unit test-integration test-e2e
.PHONY
:
build generate test test-unit test-integration test-e2e
VERSION
?=
$(
shell
tr
-d
'\r\n'
< ./cmd/server/VERSION
)
LDFLAGS
?=
-s
-w
-X
main.Version
=
$(VERSION)
build
:
go build
-o
bin/server ./cmd/server
CGO_ENABLED
=
0 go build
-ldflags
=
"
$(LDFLAGS)
"
-trimpath
-o
bin/server ./cmd/server
generate
:
go generate ./ent
go generate ./cmd/server
test
:
go
test
./...
...
...
@@ -14,4 +21,7 @@ test-integration:
go
test
-tags
=
integration ./...
test-e2e
:
go
test
-tags
=
e2e ./...
./scripts/e2e-test.sh
test-e2e-local
:
go
test
-tags
=
e2e
-v
-timeout
=
300s ./internal/integration/...
backend/cmd/jwtgen/main.go
View file @
3d79773b
...
...
@@ -17,7 +17,7 @@ func main() {
email
:=
flag
.
String
(
"email"
,
""
,
"Admin email to issue a JWT for (defaults to first active admin)"
)
flag
.
Parse
()
cfg
,
err
:=
config
.
Load
()
cfg
,
err
:=
config
.
Load
ForBootstrap
()
if
err
!=
nil
{
log
.
Fatalf
(
"failed to load config: %v"
,
err
)
}
...
...
@@ -33,7 +33,7 @@ func main() {
}()
userRepo
:=
repository
.
NewUserRepository
(
client
,
sqlDB
)
authService
:=
service
.
NewAuthService
(
userRepo
,
nil
,
nil
,
cfg
,
nil
,
nil
,
nil
,
nil
,
nil
)
authService
:=
service
.
NewAuthService
(
userRepo
,
nil
,
nil
,
cfg
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
5
*
time
.
Second
)
defer
cancel
()
...
...
backend/cmd/server/VERSION
View file @
3d79773b
0.1.76
\ No newline at end of file
0.1.88
\ No newline at end of file
backend/cmd/server/main.go
View file @
3d79773b
...
...
@@ -8,7 +8,6 @@ import (
"errors"
"flag"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
...
...
@@ -19,11 +18,14 @@ import (
_
"github.com/Wei-Shaw/sub2api/ent/runtime"
"github.com/Wei-Shaw/sub2api/internal/config"
"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/setup"
"github.com/Wei-Shaw/sub2api/internal/web"
"github.com/gin-gonic/gin"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
//go:embed VERSION
...
...
@@ -38,7 +40,12 @@ var (
)
func
init
()
{
// Read version from embedded VERSION file
// 如果 Version 已通过 ldflags 注入(例如 -X main.Version=...),则不要覆盖。
if
strings
.
TrimSpace
(
Version
)
!=
""
{
return
}
// 默认从 embedded VERSION 文件读取版本号(编译期打包进二进制)。
Version
=
strings
.
TrimSpace
(
embeddedVersion
)
if
Version
==
""
{
Version
=
"0.0.0-dev"
...
...
@@ -47,22 +54,9 @@ func init() {
// initLogger configures the default slog handler based on gin.Mode().
// 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
()
{
// Initialize slog logger based on gin mode
initLogger
()
logger
.
InitBootstrap
()
defer
logger
.
Sync
()
// Parse command line flags
setupMode
:=
flag
.
Bool
(
"setup"
,
false
,
"Run setup wizard in CLI mode"
)
...
...
@@ -106,7 +100,7 @@ func runSetupServer() {
r
:=
gin
.
New
()
r
.
Use
(
middleware
.
Recovery
())
r
.
Use
(
middleware
.
CORS
(
config
.
CORSConfig
{}))
r
.
Use
(
middleware
.
SecurityHeaders
(
config
.
CSPConfig
{
Enabled
:
true
,
Policy
:
config
.
DefaultCSPPolicy
}))
r
.
Use
(
middleware
.
SecurityHeaders
(
config
.
CSPConfig
{
Enabled
:
true
,
Policy
:
config
.
DefaultCSPPolicy
}
,
nil
))
// Register setup routes
setup
.
RegisterRoutes
(
r
)
...
...
@@ -122,16 +116,26 @@ func runSetupServer() {
log
.
Printf
(
"Setup wizard available at http://%s"
,
addr
)
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
)
}
}
func
runMainServer
()
{
cfg
,
err
:=
config
.
Load
()
cfg
,
err
:=
config
.
Load
ForBootstrap
()
if
err
!=
nil
{
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
{
log
.
Println
(
"⚠️ WARNING: Running in SIMPLE mode - billing and quota checks are DISABLED"
)
}
...
...
backend/cmd/server/wire.go
View file @
3d79773b
...
...
@@ -7,6 +7,7 @@ import (
"context"
"log"
"net/http"
"sync"
"time"
"github.com/Wei-Shaw/sub2api/ent"
...
...
@@ -67,28 +68,36 @@ func provideCleanup(
opsAlertEvaluator
*
service
.
OpsAlertEvaluatorService
,
opsCleanup
*
service
.
OpsCleanupService
,
opsScheduledReport
*
service
.
OpsScheduledReportService
,
opsSystemLogSink
*
service
.
OpsSystemLogSink
,
soraMediaCleanup
*
service
.
SoraMediaCleanupService
,
schedulerSnapshot
*
service
.
SchedulerSnapshotService
,
tokenRefresh
*
service
.
TokenRefreshService
,
accountExpiry
*
service
.
AccountExpiryService
,
subscriptionExpiry
*
service
.
SubscriptionExpiryService
,
usageCleanup
*
service
.
UsageCleanupService
,
idempotencyCleanup
*
service
.
IdempotencyCleanupService
,
pricing
*
service
.
PricingService
,
emailQueue
*
service
.
EmailQueueService
,
billingCache
*
service
.
BillingCacheService
,
usageRecordWorkerPool
*
service
.
UsageRecordWorkerPool
,
subscriptionService
*
service
.
SubscriptionService
,
oauth
*
service
.
OAuthService
,
openaiOAuth
*
service
.
OpenAIOAuthService
,
geminiOAuth
*
service
.
GeminiOAuthService
,
antigravityOAuth
*
service
.
AntigravityOAuthService
,
openAIGateway
*
service
.
OpenAIGatewayService
,
)
func
()
{
return
func
()
{
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
10
*
time
.
Second
)
defer
cancel
()
// Cleanup steps in reverse dependency order
cleanupSteps
:=
[]
struct
{
type
cleanupStep
struct
{
name
string
fn
func
()
error
}{
}
// 应用层清理步骤可并行执行,基础设施资源(Redis/Ent)最后按顺序关闭。
parallelSteps
:=
[]
cleanupStep
{
{
"OpsScheduledReportService"
,
func
()
error
{
if
opsScheduledReport
!=
nil
{
opsScheduledReport
.
Stop
()
...
...
@@ -101,6 +110,18 @@ func provideCleanup(
}
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
{
if
opsAlertEvaluator
!=
nil
{
opsAlertEvaluator
.
Stop
()
...
...
@@ -131,6 +152,12 @@ func provideCleanup(
}
return
nil
}},
{
"IdempotencyCleanupService"
,
func
()
error
{
if
idempotencyCleanup
!=
nil
{
idempotencyCleanup
.
Stop
()
}
return
nil
}},
{
"TokenRefreshService"
,
func
()
error
{
tokenRefresh
.
Stop
()
return
nil
...
...
@@ -143,6 +170,12 @@ func provideCleanup(
subscriptionExpiry
.
Stop
()
return
nil
}},
{
"SubscriptionService"
,
func
()
error
{
if
subscriptionService
!=
nil
{
subscriptionService
.
Stop
()
}
return
nil
}},
{
"PricingService"
,
func
()
error
{
pricing
.
Stop
()
return
nil
...
...
@@ -155,6 +188,12 @@ func provideCleanup(
billingCache
.
Stop
()
return
nil
}},
{
"UsageRecordWorkerPool"
,
func
()
error
{
if
usageRecordWorkerPool
!=
nil
{
usageRecordWorkerPool
.
Stop
()
}
return
nil
}},
{
"OAuthService"
,
func
()
error
{
oauth
.
Stop
()
return
nil
...
...
@@ -171,23 +210,60 @@ func provideCleanup(
antigravityOAuth
.
Stop
()
return
nil
}},
{
"OpenAIWSPool"
,
func
()
error
{
if
openAIGateway
!=
nil
{
openAIGateway
.
CloseOpenAIWSPool
()
}
return
nil
}},
}
infraSteps
:=
[]
cleanupStep
{
{
"Redis"
,
func
()
error
{
if
rdb
==
nil
{
return
nil
}
return
rdb
.
Close
()
}},
{
"Ent"
,
func
()
error
{
if
entClient
==
nil
{
return
nil
}
return
entClient
.
Close
()
}},
}
for
_
,
step
:=
range
cleanupSteps
{
if
err
:=
step
.
fn
();
err
!=
nil
{
log
.
Printf
(
"[Cleanup] %s failed: %v"
,
step
.
name
,
err
)
// Continue with remaining cleanup steps even if one fails
}
else
{
runParallel
:=
func
(
steps
[]
cleanupStep
)
{
var
wg
sync
.
WaitGroup
for
i
:=
range
steps
{
step
:=
steps
[
i
]
wg
.
Add
(
1
)
go
func
()
{
defer
wg
.
Done
()
if
err
:=
step
.
fn
();
err
!=
nil
{
log
.
Printf
(
"[Cleanup] %s failed: %v"
,
step
.
name
,
err
)
return
}
log
.
Printf
(
"[Cleanup] %s succeeded"
,
step
.
name
)
}()
}
wg
.
Wait
()
}
runSequential
:=
func
(
steps
[]
cleanupStep
)
{
for
i
:=
range
steps
{
step
:=
steps
[
i
]
if
err
:=
step
.
fn
();
err
!=
nil
{
log
.
Printf
(
"[Cleanup] %s failed: %v"
,
step
.
name
,
err
)
continue
}
log
.
Printf
(
"[Cleanup] %s succeeded"
,
step
.
name
)
}
}
runParallel
(
parallelSteps
)
runSequential
(
infraSteps
)
// Check if context timed out
select
{
case
<-
ctx
.
Done
()
:
...
...
backend/cmd/server/wire_gen.go
View file @
3d79773b
This diff is collapsed.
Click to expand it.
backend/cmd/server/wire_gen_test.go
0 → 100644
View file @
3d79773b
package
main
import
(
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/handler"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/require"
)
func
TestProvideServiceBuildInfo
(
t
*
testing
.
T
)
{
in
:=
handler
.
BuildInfo
{
Version
:
"v-test"
,
BuildType
:
"release"
,
}
out
:=
provideServiceBuildInfo
(
in
)
require
.
Equal
(
t
,
in
.
Version
,
out
.
Version
)
require
.
Equal
(
t
,
in
.
BuildType
,
out
.
BuildType
)
}
func
TestProvideCleanup_WithMinimalDependencies_NoPanic
(
t
*
testing
.
T
)
{
cfg
:=
&
config
.
Config
{}
oauthSvc
:=
service
.
NewOAuthService
(
nil
,
nil
)
openAIOAuthSvc
:=
service
.
NewOpenAIOAuthService
(
nil
,
nil
)
geminiOAuthSvc
:=
service
.
NewGeminiOAuthService
(
nil
,
nil
,
nil
,
nil
,
cfg
)
antigravityOAuthSvc
:=
service
.
NewAntigravityOAuthService
(
nil
)
tokenRefreshSvc
:=
service
.
NewTokenRefreshService
(
nil
,
oauthSvc
,
openAIOAuthSvc
,
geminiOAuthSvc
,
antigravityOAuthSvc
,
nil
,
nil
,
cfg
,
nil
,
)
accountExpirySvc
:=
service
.
NewAccountExpiryService
(
nil
,
time
.
Second
)
subscriptionExpirySvc
:=
service
.
NewSubscriptionExpiryService
(
nil
,
time
.
Second
)
pricingSvc
:=
service
.
NewPricingService
(
cfg
,
nil
)
emailQueueSvc
:=
service
.
NewEmailQueueService
(
nil
,
1
)
billingCacheSvc
:=
service
.
NewBillingCacheService
(
nil
,
nil
,
nil
,
nil
,
cfg
)
idempotencyCleanupSvc
:=
service
.
NewIdempotencyCleanupService
(
nil
,
cfg
)
schedulerSnapshotSvc
:=
service
.
NewSchedulerSnapshotService
(
nil
,
nil
,
nil
,
nil
,
cfg
)
opsSystemLogSinkSvc
:=
service
.
NewOpsSystemLogSink
(
nil
)
cleanup
:=
provideCleanup
(
nil
,
// entClient
nil
,
// redis
&
service
.
OpsMetricsCollector
{},
&
service
.
OpsAggregationService
{},
&
service
.
OpsAlertEvaluatorService
{},
&
service
.
OpsCleanupService
{},
&
service
.
OpsScheduledReportService
{},
opsSystemLogSinkSvc
,
&
service
.
SoraMediaCleanupService
{},
schedulerSnapshotSvc
,
tokenRefreshSvc
,
accountExpirySvc
,
subscriptionExpirySvc
,
&
service
.
UsageCleanupService
{},
idempotencyCleanupSvc
,
pricingSvc
,
emailQueueSvc
,
billingCacheSvc
,
&
service
.
UsageRecordWorkerPool
{},
&
service
.
SubscriptionService
{},
oauthSvc
,
openAIOAuthSvc
,
geminiOAuthSvc
,
antigravityOAuthSvc
,
nil
,
// openAIGateway
)
require
.
NotPanics
(
t
,
func
()
{
cleanup
()
})
}
backend/ent/account.go
View file @
3d79773b
...
...
@@ -63,6 +63,10 @@ type Account struct {
RateLimitResetAt
*
time
.
Time
`json:"rate_limit_reset_at,omitempty"`
// OverloadUntil holds the value of the "overload_until" field.
OverloadUntil
*
time
.
Time
`json:"overload_until,omitempty"`
// TempUnschedulableUntil holds the value of the "temp_unschedulable_until" field.
TempUnschedulableUntil
*
time
.
Time
`json:"temp_unschedulable_until,omitempty"`
// TempUnschedulableReason holds the value of the "temp_unschedulable_reason" field.
TempUnschedulableReason
*
string
`json:"temp_unschedulable_reason,omitempty"`
// SessionWindowStart holds the value of the "session_window_start" field.
SessionWindowStart
*
time
.
Time
`json:"session_window_start,omitempty"`
// SessionWindowEnd holds the value of the "session_window_end" field.
...
...
@@ -141,9 +145,9 @@ func (*Account) scanValues(columns []string) ([]any, error) {
values
[
i
]
=
new
(
sql
.
NullFloat64
)
case
account
.
FieldID
,
account
.
FieldProxyID
,
account
.
FieldConcurrency
,
account
.
FieldPriority
:
values
[
i
]
=
new
(
sql
.
NullInt64
)
case
account
.
FieldName
,
account
.
FieldNotes
,
account
.
FieldPlatform
,
account
.
FieldType
,
account
.
FieldStatus
,
account
.
FieldErrorMessage
,
account
.
FieldSessionWindowStatus
:
case
account
.
FieldName
,
account
.
FieldNotes
,
account
.
FieldPlatform
,
account
.
FieldType
,
account
.
FieldStatus
,
account
.
FieldErrorMessage
,
account
.
FieldTempUnschedulableReason
,
account
.
FieldSessionWindowStatus
:
values
[
i
]
=
new
(
sql
.
NullString
)
case
account
.
FieldCreatedAt
,
account
.
FieldUpdatedAt
,
account
.
FieldDeletedAt
,
account
.
FieldLastUsedAt
,
account
.
FieldExpiresAt
,
account
.
FieldRateLimitedAt
,
account
.
FieldRateLimitResetAt
,
account
.
FieldOverloadUntil
,
account
.
FieldSessionWindowStart
,
account
.
FieldSessionWindowEnd
:
case
account
.
FieldCreatedAt
,
account
.
FieldUpdatedAt
,
account
.
FieldDeletedAt
,
account
.
FieldLastUsedAt
,
account
.
FieldExpiresAt
,
account
.
FieldRateLimitedAt
,
account
.
FieldRateLimitResetAt
,
account
.
FieldOverloadUntil
,
account
.
FieldTempUnschedulableUntil
,
account
.
FieldSessionWindowStart
,
account
.
FieldSessionWindowEnd
:
values
[
i
]
=
new
(
sql
.
NullTime
)
default
:
values
[
i
]
=
new
(
sql
.
UnknownType
)
...
...
@@ -311,6 +315,20 @@ func (_m *Account) assignValues(columns []string, values []any) error {
_m
.
OverloadUntil
=
new
(
time
.
Time
)
*
_m
.
OverloadUntil
=
value
.
Time
}
case
account
.
FieldTempUnschedulableUntil
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullTime
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field temp_unschedulable_until"
,
values
[
i
])
}
else
if
value
.
Valid
{
_m
.
TempUnschedulableUntil
=
new
(
time
.
Time
)
*
_m
.
TempUnschedulableUntil
=
value
.
Time
}
case
account
.
FieldTempUnschedulableReason
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullString
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field temp_unschedulable_reason"
,
values
[
i
])
}
else
if
value
.
Valid
{
_m
.
TempUnschedulableReason
=
new
(
string
)
*
_m
.
TempUnschedulableReason
=
value
.
String
}
case
account
.
FieldSessionWindowStart
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullTime
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field session_window_start"
,
values
[
i
])
...
...
@@ -472,6 +490,16 @@ func (_m *Account) String() string {
builder
.
WriteString
(
v
.
Format
(
time
.
ANSIC
))
}
builder
.
WriteString
(
", "
)
if
v
:=
_m
.
TempUnschedulableUntil
;
v
!=
nil
{
builder
.
WriteString
(
"temp_unschedulable_until="
)
builder
.
WriteString
(
v
.
Format
(
time
.
ANSIC
))
}
builder
.
WriteString
(
", "
)
if
v
:=
_m
.
TempUnschedulableReason
;
v
!=
nil
{
builder
.
WriteString
(
"temp_unschedulable_reason="
)
builder
.
WriteString
(
*
v
)
}
builder
.
WriteString
(
", "
)
if
v
:=
_m
.
SessionWindowStart
;
v
!=
nil
{
builder
.
WriteString
(
"session_window_start="
)
builder
.
WriteString
(
v
.
Format
(
time
.
ANSIC
))
...
...
backend/ent/account/account.go
View file @
3d79773b
...
...
@@ -59,6 +59,10 @@ const (
FieldRateLimitResetAt
=
"rate_limit_reset_at"
// FieldOverloadUntil holds the string denoting the overload_until field in the database.
FieldOverloadUntil
=
"overload_until"
// FieldTempUnschedulableUntil holds the string denoting the temp_unschedulable_until field in the database.
FieldTempUnschedulableUntil
=
"temp_unschedulable_until"
// FieldTempUnschedulableReason holds the string denoting the temp_unschedulable_reason field in the database.
FieldTempUnschedulableReason
=
"temp_unschedulable_reason"
// FieldSessionWindowStart holds the string denoting the session_window_start field in the database.
FieldSessionWindowStart
=
"session_window_start"
// FieldSessionWindowEnd holds the string denoting the session_window_end field in the database.
...
...
@@ -128,6 +132,8 @@ var Columns = []string{
FieldRateLimitedAt
,
FieldRateLimitResetAt
,
FieldOverloadUntil
,
FieldTempUnschedulableUntil
,
FieldTempUnschedulableReason
,
FieldSessionWindowStart
,
FieldSessionWindowEnd
,
FieldSessionWindowStatus
,
...
...
@@ -299,6 +305,16 @@ func ByOverloadUntil(opts ...sql.OrderTermOption) OrderOption {
return
sql
.
OrderByField
(
FieldOverloadUntil
,
opts
...
)
.
ToFunc
()
}
// ByTempUnschedulableUntil orders the results by the temp_unschedulable_until field.
func
ByTempUnschedulableUntil
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldTempUnschedulableUntil
,
opts
...
)
.
ToFunc
()
}
// ByTempUnschedulableReason orders the results by the temp_unschedulable_reason field.
func
ByTempUnschedulableReason
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldTempUnschedulableReason
,
opts
...
)
.
ToFunc
()
}
// BySessionWindowStart orders the results by the session_window_start field.
func
BySessionWindowStart
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldSessionWindowStart
,
opts
...
)
.
ToFunc
()
...
...
backend/ent/account/where.go
View file @
3d79773b
...
...
@@ -155,6 +155,16 @@ func OverloadUntil(v time.Time) predicate.Account {
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldOverloadUntil
,
v
))
}
// TempUnschedulableUntil applies equality check predicate on the "temp_unschedulable_until" field. It's identical to TempUnschedulableUntilEQ.
func
TempUnschedulableUntil
(
v
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldTempUnschedulableUntil
,
v
))
}
// TempUnschedulableReason applies equality check predicate on the "temp_unschedulable_reason" field. It's identical to TempUnschedulableReasonEQ.
func
TempUnschedulableReason
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldTempUnschedulableReason
,
v
))
}
// SessionWindowStart applies equality check predicate on the "session_window_start" field. It's identical to SessionWindowStartEQ.
func
SessionWindowStart
(
v
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldSessionWindowStart
,
v
))
...
...
@@ -1130,6 +1140,131 @@ func OverloadUntilNotNil() predicate.Account {
return
predicate
.
Account
(
sql
.
FieldNotNull
(
FieldOverloadUntil
))
}
// TempUnschedulableUntilEQ applies the EQ predicate on the "temp_unschedulable_until" field.
func
TempUnschedulableUntilEQ
(
v
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldTempUnschedulableUntil
,
v
))
}
// TempUnschedulableUntilNEQ applies the NEQ predicate on the "temp_unschedulable_until" field.
func
TempUnschedulableUntilNEQ
(
v
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldNEQ
(
FieldTempUnschedulableUntil
,
v
))
}
// TempUnschedulableUntilIn applies the In predicate on the "temp_unschedulable_until" field.
func
TempUnschedulableUntilIn
(
vs
...
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldIn
(
FieldTempUnschedulableUntil
,
vs
...
))
}
// TempUnschedulableUntilNotIn applies the NotIn predicate on the "temp_unschedulable_until" field.
func
TempUnschedulableUntilNotIn
(
vs
...
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldNotIn
(
FieldTempUnschedulableUntil
,
vs
...
))
}
// TempUnschedulableUntilGT applies the GT predicate on the "temp_unschedulable_until" field.
func
TempUnschedulableUntilGT
(
v
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldGT
(
FieldTempUnschedulableUntil
,
v
))
}
// TempUnschedulableUntilGTE applies the GTE predicate on the "temp_unschedulable_until" field.
func
TempUnschedulableUntilGTE
(
v
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldGTE
(
FieldTempUnschedulableUntil
,
v
))
}
// TempUnschedulableUntilLT applies the LT predicate on the "temp_unschedulable_until" field.
func
TempUnschedulableUntilLT
(
v
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldLT
(
FieldTempUnschedulableUntil
,
v
))
}
// TempUnschedulableUntilLTE applies the LTE predicate on the "temp_unschedulable_until" field.
func
TempUnschedulableUntilLTE
(
v
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldLTE
(
FieldTempUnschedulableUntil
,
v
))
}
// TempUnschedulableUntilIsNil applies the IsNil predicate on the "temp_unschedulable_until" field.
func
TempUnschedulableUntilIsNil
()
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldIsNull
(
FieldTempUnschedulableUntil
))
}
// TempUnschedulableUntilNotNil applies the NotNil predicate on the "temp_unschedulable_until" field.
func
TempUnschedulableUntilNotNil
()
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldNotNull
(
FieldTempUnschedulableUntil
))
}
// TempUnschedulableReasonEQ applies the EQ predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonEQ
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldTempUnschedulableReason
,
v
))
}
// TempUnschedulableReasonNEQ applies the NEQ predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonNEQ
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldNEQ
(
FieldTempUnschedulableReason
,
v
))
}
// TempUnschedulableReasonIn applies the In predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonIn
(
vs
...
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldIn
(
FieldTempUnschedulableReason
,
vs
...
))
}
// TempUnschedulableReasonNotIn applies the NotIn predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonNotIn
(
vs
...
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldNotIn
(
FieldTempUnschedulableReason
,
vs
...
))
}
// TempUnschedulableReasonGT applies the GT predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonGT
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldGT
(
FieldTempUnschedulableReason
,
v
))
}
// TempUnschedulableReasonGTE applies the GTE predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonGTE
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldGTE
(
FieldTempUnschedulableReason
,
v
))
}
// TempUnschedulableReasonLT applies the LT predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonLT
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldLT
(
FieldTempUnschedulableReason
,
v
))
}
// TempUnschedulableReasonLTE applies the LTE predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonLTE
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldLTE
(
FieldTempUnschedulableReason
,
v
))
}
// TempUnschedulableReasonContains applies the Contains predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonContains
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldContains
(
FieldTempUnschedulableReason
,
v
))
}
// TempUnschedulableReasonHasPrefix applies the HasPrefix predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonHasPrefix
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldHasPrefix
(
FieldTempUnschedulableReason
,
v
))
}
// TempUnschedulableReasonHasSuffix applies the HasSuffix predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonHasSuffix
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldHasSuffix
(
FieldTempUnschedulableReason
,
v
))
}
// TempUnschedulableReasonIsNil applies the IsNil predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonIsNil
()
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldIsNull
(
FieldTempUnschedulableReason
))
}
// TempUnschedulableReasonNotNil applies the NotNil predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonNotNil
()
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldNotNull
(
FieldTempUnschedulableReason
))
}
// TempUnschedulableReasonEqualFold applies the EqualFold predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonEqualFold
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEqualFold
(
FieldTempUnschedulableReason
,
v
))
}
// TempUnschedulableReasonContainsFold applies the ContainsFold predicate on the "temp_unschedulable_reason" field.
func
TempUnschedulableReasonContainsFold
(
v
string
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldContainsFold
(
FieldTempUnschedulableReason
,
v
))
}
// SessionWindowStartEQ applies the EQ predicate on the "session_window_start" field.
func
SessionWindowStartEQ
(
v
time
.
Time
)
predicate
.
Account
{
return
predicate
.
Account
(
sql
.
FieldEQ
(
FieldSessionWindowStart
,
v
))
...
...
Prev
1
2
3
4
5
…
37
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment