Commit 3d79773b authored by kyx236's avatar kyx236
Browse files

Merge branch 'main' of https://github.com/james-6-23/sub2api

parents 6aa8cbbf 742e73c9
...@@ -11,8 +11,8 @@ jobs: ...@@ -11,8 +11,8 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: backend/go.mod go-version-file: backend/go.mod
check-latest: false check-latest: false
...@@ -30,8 +30,8 @@ jobs: ...@@ -30,8 +30,8 @@ jobs:
golangci-lint: golangci-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version-file: backend/go.mod go-version-file: backend/go.mod
check-latest: false check-latest: false
...@@ -43,5 +43,5 @@ jobs: ...@@ -43,5 +43,5 @@ jobs:
uses: golangci/golangci-lint-action@v9 uses: golangci/golangci-lint-action@v9
with: with:
version: v2.7 version: v2.7
args: --timeout=5m args: --timeout=30m
working-directory: backend working-directory: backend
\ No newline at end of file
...@@ -31,7 +31,7 @@ jobs: ...@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Update VERSION file - name: Update VERSION file
run: | run: |
...@@ -45,7 +45,7 @@ jobs: ...@@ -45,7 +45,7 @@ jobs:
echo "Updated VERSION file to: $VERSION" echo "Updated VERSION file to: $VERSION"
- name: Upload VERSION artifact - name: Upload VERSION artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: version-file name: version-file
path: backend/cmd/server/VERSION path: backend/cmd/server/VERSION
...@@ -55,7 +55,7 @@ jobs: ...@@ -55,7 +55,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
...@@ -63,7 +63,7 @@ jobs: ...@@ -63,7 +63,7 @@ jobs:
version: 9 version: 9
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: '20' node-version: '20'
cache: 'pnpm' cache: 'pnpm'
...@@ -78,7 +78,7 @@ jobs: ...@@ -78,7 +78,7 @@ jobs:
working-directory: frontend working-directory: frontend
- name: Upload frontend artifact - name: Upload frontend artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: frontend-dist name: frontend-dist
path: backend/internal/web/dist/ path: backend/internal/web/dist/
...@@ -89,25 +89,25 @@ jobs: ...@@ -89,25 +89,25 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.event.inputs.tag || github.ref }} ref: ${{ github.event.inputs.tag || github.ref }}
- name: Download VERSION artifact - name: Download VERSION artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: version-file name: version-file
path: backend/cmd/server/ path: backend/cmd/server/
- name: Download frontend artifact - name: Download frontend artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: frontend-dist name: frontend-dist
path: backend/internal/web/dist/ path: backend/internal/web/dist/
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: backend/go.mod go-version-file: backend/go.mod
check-latest: false check-latest: false
...@@ -173,7 +173,7 @@ jobs: ...@@ -173,7 +173,7 @@ jobs:
run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT run: echo "owner=$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v7
with: with:
version: '~> v2' version: '~> v2'
args: release --clean --skip=validate ${{ env.SIMPLE_RELEASE == 'true' && '--config=.goreleaser.simple.yaml' || '' }} args: release --clean --skip=validate ${{ env.SIMPLE_RELEASE == 'true' && '--config=.goreleaser.simple.yaml' || '' }}
...@@ -188,7 +188,7 @@ jobs: ...@@ -188,7 +188,7 @@ jobs:
# Update DockerHub description # Update DockerHub description
- name: Update DockerHub description - name: Update DockerHub description
if: ${{ env.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }} if: ${{ env.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }}
uses: peter-evans/dockerhub-description@v4 uses: peter-evans/dockerhub-description@v5
env: env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
with: with:
......
...@@ -12,10 +12,11 @@ permissions: ...@@ -12,10 +12,11 @@ permissions:
jobs: jobs:
backend-security: backend-security:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: backend/go.mod go-version-file: backend/go.mod
check-latest: false check-latest: false
...@@ -28,22 +29,17 @@ jobs: ...@@ -28,22 +29,17 @@ jobs:
run: | run: |
go install golang.org/x/vuln/cmd/govulncheck@latest go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./... 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: frontend-security:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up pnpm - name: Set up pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
version: 9 version: 9
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: '20' node-version: '20'
cache: 'pnpm' cache: 'pnpm'
......
...@@ -116,17 +116,20 @@ backend/.installed ...@@ -116,17 +116,20 @@ backend/.installed
# =================== # ===================
tests tests
CLAUDE.md CLAUDE.md
AGENTS.md
.claude .claude
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
deploy/docker-compose.override.yml deploy/docker-compose.override.yml
.gocache/ .gocache/
vite.config.js vite.config.js
docs/* docs/*
.serena/ .serena/
\ No newline at end of file .codex/
frontend/coverage/
aicodex
output/
...@@ -209,7 +209,30 @@ git add ent/ # 生成的文件也要提交 ...@@ -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 前务必本地验证:
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
ARG NODE_IMAGE=node:24-alpine ARG NODE_IMAGE=node:24-alpine
ARG GOLANG_IMAGE=golang:1.25.7-alpine ARG GOLANG_IMAGE=golang:1.25.7-alpine
ARG ALPINE_IMAGE=alpine:3.20 ARG ALPINE_IMAGE=alpine:3.21
ARG GOPROXY=https://goproxy.cn,direct ARG GOPROXY=https://goproxy.cn,direct
ARG GOSUMDB=sum.golang.google.cn ARG GOSUMDB=sum.golang.google.cn
...@@ -36,7 +36,7 @@ RUN pnpm run build ...@@ -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,14 @@ COPY backend/ ./ ...@@ -61,9 +61,14 @@ 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" \
-trimpath \
-o /app/sub2api \ -o /app/sub2api \
./cmd/server ./cmd/server
...@@ -81,7 +86,6 @@ LABEL org.opencontainers.image.source="https://github.com/Wei-Shaw/sub2api" ...@@ -81,7 +86,6 @@ LABEL org.opencontainers.image.source="https://github.com/Wei-Shaw/sub2api"
RUN apk add --no-cache \ RUN apk add --no-cache \
ca-certificates \ ca-certificates \
tzdata \ tzdata \
curl \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*
# Create non-root user # Create non-root user
...@@ -91,11 +95,12 @@ RUN addgroup -g 1000 sub2api && \ ...@@ -91,11 +95,12 @@ RUN addgroup -g 1000 sub2api && \
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
# Copy binary from builder # Copy binary/resources with ownership to avoid extra full-layer chown copy
COPY --from=backend-builder /app/sub2api /app/sub2api 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 # 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 # Switch to non-root user
USER sub2api USER sub2api
...@@ -105,7 +110,7 @@ EXPOSE 8080 ...@@ -105,7 +110,7 @@ EXPOSE 8080
# Health check # Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ 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 # Run the application
ENTRYPOINT ["/app/sub2api"] ENTRYPOINT ["/app/sub2api"]
.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 build: build-backend build-frontend
...@@ -11,6 +11,10 @@ build-backend: ...@@ -11,6 +11,10 @@ build-backend:
build-frontend: build-frontend:
@pnpm --dir frontend run build @pnpm --dir frontend run build
# 编译 datamanagementd(宿主机数据管理进程)
build-datamanagementd:
@cd datamanagement && go build -o datamanagementd ./cmd/datamanagementd
# 运行测试(后端 + 前端) # 运行测试(后端 + 前端)
test: test-backend test-frontend test: test-backend test-frontend
...@@ -20,3 +24,9 @@ test-backend: ...@@ -20,3 +24,9 @@ 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
test-datamanagementd:
@cd datamanagement && go test ./...
secret-scan:
@python3 tools/secret_scan.py
...@@ -54,6 +54,7 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot ...@@ -54,6 +54,7 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot
## Documentation ## Documentation
- Dependency Security: `docs/dependency-security.md` - Dependency Security: `docs/dependency-security.md`
- Admin Payment Integration API: `docs/ADMIN_PAYMENT_INTEGRATION_API.md`
--- ---
...@@ -363,6 +364,12 @@ default: ...@@ -363,6 +364,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
......
...@@ -62,8 +62,6 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅( ...@@ -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` - 当请求包含 `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” 错误。 - 若依赖上游历史记录,网关会强制 `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 ...@@ -244,6 +242,18 @@ docker-compose -f docker-compose.local.yml logs -f sub2api
**推荐:** 使用 `docker-compose.local.yml`(脚本部署)以便更轻松地管理数据。 **推荐:** 使用 `docker-compose.local.yml`(脚本部署)以便更轻松地管理数据。
#### 启用“数据管理”功能(datamanagementd)
如需启用管理后台“数据管理”,需要额外部署宿主机数据管理进程 `datamanagementd`
关键点:
- 主进程固定探测:`/tmp/sub2api-datamanagement.sock`
- 只有该 Socket 可连通时,数据管理功能才会开启
- Docker 场景需将宿主机 Socket 挂载到容器同路径
详细部署步骤见:`deploy/DATAMANAGEMENTD_CN.md`
#### 访问 #### 访问
在浏览器中打开 `http://你的服务器IP:8080` 在浏览器中打开 `http://你的服务器IP:8080`
...@@ -370,6 +380,33 @@ default: ...@@ -370,6 +380,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 +420,14 @@ default: ...@@ -383,6 +420,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 +473,29 @@ Invalid base URL: invalid url scheme: http ...@@ -428,6 +473,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,6 +5,7 @@ linters: ...@@ -5,6 +5,7 @@ linters:
enable: enable:
- depguard - depguard
- errcheck - errcheck
- gosec
- govet - govet
- ineffassign - ineffassign
- staticcheck - staticcheck
...@@ -42,6 +43,22 @@ linters: ...@@ -42,6 +43,22 @@ linters:
desc: "handler must not import gorm" desc: "handler must not import gorm"
- pkg: github.com/redis/go-redis/v9 - pkg: github.com/redis/go-redis/v9
desc: "handler must not import redis" 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: errcheck:
# Report about not checking of errors in type assertions: `a := b.(MyStruct)`. # Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
# Such cases aren't reported by default. # Such cases aren't reported by default.
......
.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: 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: test:
go test ./... go test ./...
...@@ -14,4 +21,7 @@ test-integration: ...@@ -14,4 +21,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() { ...@@ -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)
} }
...@@ -33,7 +33,7 @@ func main() { ...@@ -33,7 +33,7 @@ func main() {
}() }()
userRepo := repository.NewUserRepository(client, sqlDB) 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) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
......
0.1.76 0.1.88
\ No newline at end of file \ No newline at end of file
...@@ -8,7 +8,6 @@ import ( ...@@ -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 ( ...@@ -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 ( ...@@ -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() { ...@@ -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")
...@@ -106,7 +100,7 @@ func runSetupServer() { ...@@ -106,7 +100,7 @@ func runSetupServer() {
r := gin.New() r := gin.New()
r.Use(middleware.Recovery()) r.Use(middleware.Recovery())
r.Use(middleware.CORS(config.CORSConfig{})) 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 // Register setup routes
setup.RegisterRoutes(r) setup.RegisterRoutes(r)
...@@ -122,16 +116,26 @@ func runSetupServer() { ...@@ -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")
} }
......
...@@ -7,6 +7,7 @@ import ( ...@@ -7,6 +7,7 @@ import (
"context" "context"
"log" "log"
"net/http" "net/http"
"sync"
"time" "time"
"github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent"
...@@ -67,28 +68,36 @@ func provideCleanup( ...@@ -67,28 +68,36 @@ 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,
antigravityOAuth *service.AntigravityOAuthService, antigravityOAuth *service.AntigravityOAuthService,
openAIGateway *service.OpenAIGatewayService,
) func() { ) func() {
return func() { return func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
// Cleanup steps in reverse dependency order type cleanupStep struct {
cleanupSteps := []struct {
name string name string
fn func() error fn func() error
}{ }
// 应用层清理步骤可并行执行,基础设施资源(Redis/Ent)最后按顺序关闭。
parallelSteps := []cleanupStep{
{"OpsScheduledReportService", func() error { {"OpsScheduledReportService", func() error {
if opsScheduledReport != nil { if opsScheduledReport != nil {
opsScheduledReport.Stop() opsScheduledReport.Stop()
...@@ -101,6 +110,18 @@ func provideCleanup( ...@@ -101,6 +110,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 +152,12 @@ func provideCleanup( ...@@ -131,6 +152,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 +170,12 @@ func provideCleanup( ...@@ -143,6 +170,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 +188,12 @@ func provideCleanup( ...@@ -155,6 +188,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
...@@ -171,23 +210,60 @@ func provideCleanup( ...@@ -171,23 +210,60 @@ func provideCleanup(
antigravityOAuth.Stop() antigravityOAuth.Stop()
return nil return nil
}}, }},
{"OpenAIWSPool", func() error {
if openAIGateway != nil {
openAIGateway.CloseOpenAIWSPool()
}
return nil
}},
}
infraSteps := []cleanupStep{
{"Redis", func() error { {"Redis", func() error {
if rdb == nil {
return nil
}
return rdb.Close() return rdb.Close()
}}, }},
{"Ent", func() error { {"Ent", func() error {
if entClient == nil {
return nil
}
return entClient.Close() return entClient.Close()
}}, }},
} }
for _, step := range cleanupSteps { runParallel := func(steps []cleanupStep) {
if err := step.fn(); err != nil { var wg sync.WaitGroup
log.Printf("[Cleanup] %s failed: %v", step.name, err) for i := range steps {
// Continue with remaining cleanup steps even if one fails step := steps[i]
} else { 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) log.Printf("[Cleanup] %s succeeded", step.name)
} }
} }
runParallel(parallelSteps)
runSequential(infraSteps)
// Check if context timed out // Check if context timed out
select { select {
case <-ctx.Done(): case <-ctx.Done():
......
This diff is collapsed.
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()
})
}
...@@ -63,6 +63,10 @@ type Account struct { ...@@ -63,6 +63,10 @@ type Account struct {
RateLimitResetAt *time.Time `json:"rate_limit_reset_at,omitempty"` RateLimitResetAt *time.Time `json:"rate_limit_reset_at,omitempty"`
// OverloadUntil holds the value of the "overload_until" field. // OverloadUntil holds the value of the "overload_until" field.
OverloadUntil *time.Time `json:"overload_until,omitempty"` 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 holds the value of the "session_window_start" field.
SessionWindowStart *time.Time `json:"session_window_start,omitempty"` SessionWindowStart *time.Time `json:"session_window_start,omitempty"`
// SessionWindowEnd holds the value of the "session_window_end" field. // SessionWindowEnd holds the value of the "session_window_end" field.
...@@ -141,9 +145,9 @@ func (*Account) scanValues(columns []string) ([]any, error) { ...@@ -141,9 +145,9 @@ func (*Account) scanValues(columns []string) ([]any, error) {
values[i] = new(sql.NullFloat64) values[i] = new(sql.NullFloat64)
case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldPriority: case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldPriority:
values[i] = new(sql.NullInt64) 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) 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) values[i] = new(sql.NullTime)
default: default:
values[i] = new(sql.UnknownType) values[i] = new(sql.UnknownType)
...@@ -311,6 +315,20 @@ func (_m *Account) assignValues(columns []string, values []any) error { ...@@ -311,6 +315,20 @@ func (_m *Account) assignValues(columns []string, values []any) error {
_m.OverloadUntil = new(time.Time) _m.OverloadUntil = new(time.Time)
*_m.OverloadUntil = value.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: case account.FieldSessionWindowStart:
if value, ok := values[i].(*sql.NullTime); !ok { if value, ok := values[i].(*sql.NullTime); !ok {
return fmt.Errorf("unexpected type %T for field session_window_start", values[i]) return fmt.Errorf("unexpected type %T for field session_window_start", values[i])
...@@ -472,6 +490,16 @@ func (_m *Account) String() string { ...@@ -472,6 +490,16 @@ func (_m *Account) String() string {
builder.WriteString(v.Format(time.ANSIC)) builder.WriteString(v.Format(time.ANSIC))
} }
builder.WriteString(", ") 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 { if v := _m.SessionWindowStart; v != nil {
builder.WriteString("session_window_start=") builder.WriteString("session_window_start=")
builder.WriteString(v.Format(time.ANSIC)) builder.WriteString(v.Format(time.ANSIC))
......
...@@ -59,6 +59,10 @@ const ( ...@@ -59,6 +59,10 @@ const (
FieldRateLimitResetAt = "rate_limit_reset_at" FieldRateLimitResetAt = "rate_limit_reset_at"
// FieldOverloadUntil holds the string denoting the overload_until field in the database. // FieldOverloadUntil holds the string denoting the overload_until field in the database.
FieldOverloadUntil = "overload_until" 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 holds the string denoting the session_window_start field in the database.
FieldSessionWindowStart = "session_window_start" FieldSessionWindowStart = "session_window_start"
// FieldSessionWindowEnd holds the string denoting the session_window_end field in the database. // FieldSessionWindowEnd holds the string denoting the session_window_end field in the database.
...@@ -128,6 +132,8 @@ var Columns = []string{ ...@@ -128,6 +132,8 @@ var Columns = []string{
FieldRateLimitedAt, FieldRateLimitedAt,
FieldRateLimitResetAt, FieldRateLimitResetAt,
FieldOverloadUntil, FieldOverloadUntil,
FieldTempUnschedulableUntil,
FieldTempUnschedulableReason,
FieldSessionWindowStart, FieldSessionWindowStart,
FieldSessionWindowEnd, FieldSessionWindowEnd,
FieldSessionWindowStatus, FieldSessionWindowStatus,
...@@ -299,6 +305,16 @@ func ByOverloadUntil(opts ...sql.OrderTermOption) OrderOption { ...@@ -299,6 +305,16 @@ func ByOverloadUntil(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldOverloadUntil, opts...).ToFunc() 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. // BySessionWindowStart orders the results by the session_window_start field.
func BySessionWindowStart(opts ...sql.OrderTermOption) OrderOption { func BySessionWindowStart(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldSessionWindowStart, opts...).ToFunc() return sql.OrderByField(FieldSessionWindowStart, opts...).ToFunc()
......
...@@ -155,6 +155,16 @@ func OverloadUntil(v time.Time) predicate.Account { ...@@ -155,6 +155,16 @@ func OverloadUntil(v time.Time) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldOverloadUntil, v)) 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. // SessionWindowStart applies equality check predicate on the "session_window_start" field. It's identical to SessionWindowStartEQ.
func SessionWindowStart(v time.Time) predicate.Account { func SessionWindowStart(v time.Time) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldSessionWindowStart, v)) return predicate.Account(sql.FieldEQ(FieldSessionWindowStart, v))
...@@ -1130,6 +1140,131 @@ func OverloadUntilNotNil() predicate.Account { ...@@ -1130,6 +1140,131 @@ func OverloadUntilNotNil() predicate.Account {
return predicate.Account(sql.FieldNotNull(FieldOverloadUntil)) 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. // SessionWindowStartEQ applies the EQ predicate on the "session_window_start" field.
func SessionWindowStartEQ(v time.Time) predicate.Account { func SessionWindowStartEQ(v time.Time) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldSessionWindowStart, v)) return predicate.Account(sql.FieldEQ(FieldSessionWindowStart, v))
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment