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
3b7a5fff
Commit
3b7a5fff
authored
Apr 27, 2026
by
陈曦
Browse files
补充openai、gemini以及流失请求的采集数据以及nfs落库
parent
8519a8eb
Pipeline
#82284
failed with stage
in 2 minutes and 21 seconds
Changes
180
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
.gitignore
View file @
3b7a5fff
docs/claude-relay-service/
docs/claude-relay-service/
.codex
# ===================
# ===================
# Go 后端
# Go 后端
...
...
REQUEST_CAPTURE_CHANGES.md
0 → 100644
View file @
3b7a5fff
# Request/Response Capture 功能变更报告
> 功能:对指定 API Key 开启请求体与响应体的双路采集,支持数据库和 NFS 两种存储方式,覆盖 Claude、OpenAI 及 Gemini 全部网关路径。
---
## 一、数据库迁移
**文件:`backend/migrations/108_request_capture_log.sql`**
```
sql
-- api_keys 新增开关字段
ALTER
TABLE
api_keys
ADD
COLUMN
IF
NOT
EXISTS
capture_requests
boolean
NOT
NULL
DEFAULT
false
;
-- 按月分区的捕获日志表
CREATE
TABLE
IF
NOT
EXISTS
request_capture_logs
(
id
bigserial
NOT
NULL
,
api_key_id
bigint
NOT
NULL
,
user_id
bigint
NOT
NULL
,
request_id
varchar
(
64
),
path
varchar
(
100
),
method
varchar
(
10
),
ip_address
varchar
(
45
),
request_body
text
,
response_body
text
,
nfs_file_path
varchar
(
500
),
created_at
timestamptz
NOT
NULL
DEFAULT
now
(),
PRIMARY
KEY
(
id
,
created_at
)
)
PARTITION
BY
RANGE
(
created_at
);
CREATE
INDEX
IF
NOT
EXISTS
idx_rcl_api_key_created
ON
request_capture_logs
(
api_key_id
,
created_at
DESC
);
CREATE
INDEX
IF
NOT
EXISTS
idx_rcl_user_id
ON
request_capture_logs
(
user_id
);
```
预建前、当、后三个月分区,后续需定期维护新分区。
---
## 二、配置
### 2.1 `backend/internal/config/config.go`(新增结构体)
```
go
type
RequestCaptureConfig
struct
{
NFSPath
string
`mapstructure:"nfs_path"`
WorkerTimeoutSeconds
int
`mapstructure:"worker_timeout_seconds"`
}
```
### 2.2 `backend/config.yaml`(新增块)
```
yaml
request_capture
:
nfs_path
:
"
/app/logs/nfs/"
worker_timeout_seconds
:
5
```
### 2.3 `deploy/.env.example`(新增两行)
```
env
REQUEST_CAPTURE_NFS_PATH=
REQUEST_CAPTURE_WORKER_TIMEOUT_SECONDS=5
```
---
## 三、核心服务
### 3.1 `backend/internal/service/request_capture_service.go`(全新文件)
| 方法 | 描述 |
|---|---|
|
`Capture(...)`
| 同步写 DB(返回 captureID),异步写 NFS 请求文件;若有 NFS 路径则将
`captureID→nfsFilePath`
存入
`sync.Map`
|
|
`CaptureResponse(captureID, responseBody)`
| 异步:更新 DB
`response_body`
;若有 NFS 路径则写
`<原文件名>_response.json`
|
|
`nfsResponseFilePath(requestPath)`
|
`xxx.json`
→
`xxx_response.json`
|
|
`writeResponseToNFS(...)`
| 写
`nfsResponseEnvelope{capture_id, created_at, body}`
|
**关键设计:**
-
`Capture()`
是
**同步**
DB 写入,保证返回 captureID 后立即可用
-
`CaptureResponse()`
是
**全异步**
,不阻塞请求响应链路
-
`sync.Map`
以 captureID 为 key 暂存 nfsFilePath,
`LoadAndDelete`
一次性消费,避免内存泄漏
**NFS 文件组织结构:**
```
{nfsPath}/
└── {YYYY-MM-DD}/
└── {apiKeyID}/
├── {unixNano}_{requestID}.json ← 请求体
└── {unixNano}_{requestID}_response.json ← 响应体
```
### 3.2 `backend/internal/repository/request_capture_log_repo.go`(全新文件)
```
go
// 实现 RequestCaptureLogRepository 接口
Create
(
ctx
,
params
)
(
int64
,
error
)
UpdateResponseBody
(
ctx
,
id
,
responseBody
)
error
```
---
## 四、Context Key
**`backend/internal/pkg/ctxkey/ctxkey.go`(新增)**
```
go
// ResponseCaptureBuffer 流式响应中收集 assistant 文本,供 request_capture 使用。
// 值类型为 *strings.Builder,由 handler 层注入,service 层只负责追加文本。
ResponseCaptureBuffer
Key
=
"ctx_response_capture_buffer"
```
流式请求的文本采集流程:
```
handler 注入 *strings.Builder 到 context
↓
service streaming handler 追加 text_delta
↓
Forward() / ForwardGemini() 读取 builder.String()
↓
ForwardResult.ResponseBody
↓
handler CaptureResponse()
```
---
## 五、各端点覆盖详情
### 5.1 `/v1/messages` → Anthropic 账号
| 位置 | 变更 |
|---|---|
|
`handler/gateway_handler.go`
|
`Capture()`
保存 captureID;流式注入
`ResponseCaptureBuffer`
;Anthropic success path 调用
`CaptureResponse()`
|
|
`handler/gateway_handler.go`
(Gemini success path)|
**新增**
`CaptureResponse(captureID, result.ResponseBody)`
(约第513行) |
|
`service/gateway_service.go`
|
`ForwardResult`
新增
`ResponseBody string`
;非流式读响应字节,流式读 context buffer |
### 5.2 `/v1/messages` → CC 转发(Anthropic CC 协议)
| 位置 | 变更 |
|---|---|
|
`handler/gateway_handler_chat_completions.go`
|
`Capture()`
+
`CaptureResponse()`
|
|
`service/gateway_forward_as_chat_completions.go`
| 非流式:marshal
`ccResp`
→
`ResponseBody`
;流式:
`textBuilder`
收集
`Delta.Content`
|
### 5.3 `/openai/v1/chat/completions`
| 位置 | 变更 |
|---|---|
|
`handler/openai_chat_completions.go`
|
`Capture()`
+
`CaptureResponse()`
|
|
`service/openai_gateway_chat_completions.go`
| 非流式:marshal
`ccResp`
;流式:
`textBuilder`
收集
`Delta.Content`
|
### 5.4 `/openai/v1/responses`(**本次重点修复**)
**问题:**
`Capture()`
返回值被丢弃,两条子路径均无
`CaptureResponse()`
调用,且
`ForwardAsAnthropic`
从未填充
`ResponseBody`
。
| 位置 | 变更 |
|---|---|
|
`handler/openai_gateway_handler.go`
|
**`Capture()` 改为保存 `captureID`**
;Sub-path 1(
`Forward()`
)和 Sub-path 2(
`ForwardAsAnthropic()`
)success block 均
**新增**
`CaptureResponse()`
|
|
`service/openai_gateway_messages.go`
(
`handleAnthropicBufferedResponse`
)|
`c.JSON`
改为
`json.Marshal + c.Data`
,
**填充 `ResponseBody`**
|
|
`service/openai_gateway_messages.go`
(
`handleAnthropicStreamingResponse`
)|
**新增**
`textBuilder`
;
`processDataLine`
中捕获
`content_block_delta / text_delta`
;
`resultWithUsage()`
加
`ResponseBody: textBuilder.String()`
|
### 5.5 `/v1/messages` → Antigravity(Gemini)账号(**本次新增**)
| 位置 | 变更 |
|---|---|
|
`handler/gateway_handler.go`
| 复用 5.1 的
`captureID`
+
`CaptureResponse`
|
|
`service/antigravity_gateway_service.go`
|
`antigravityStreamResult`
新增
`responseBody string`
;import 加
`ctxkey`
|
|
`handleClaudeStreamToNonStreaming`
|
`responseBody: string(claudeResp)`
|
|
`handleClaudeStreamingResponse`
| 读 context buffer;gjson 提取每条 SSE 中的
`response.candidates.0.content.parts.0.text`
或
`candidates.0.content.parts.0.text`
|
|
`handleGeminiStreamToNonStreaming`
|
`responseBody: strings.Join(collectedTextParts, "")`
|
|
`handleGeminiStreamingResponse`
| 读 context buffer;gjson 遍历
`candidates.0.content.parts[*].text`
|
|
`Forward()`
/
`ForwardGemini()`
| 流式从 context buffer 读;非流式从
`streamRes.responseBody`
读;
`ForwardResult.ResponseBody`
填充 |
### 5.6 `/v1/messages` → GeminiMessagesCompat 账号(**本次新增**)
| 位置 | 变更 |
|---|---|
|
`handler/gateway_handler.go`
| 复用 5.1 的
`captureID`
+
`CaptureResponse`
|
|
`service/gemini_messages_compat_service.go`
|
`geminiStreamResult`
新增
`responseBody string`
|
|
`handleStreamingResponse`
|
`responseBody: seenText`
(函数内已有完整文本累积) |
|
`handleNonStreamingResponse`
| 签名改为
`(string, *ClaudeUsage, error)`
;
`c.JSON`
改为
`json.Marshal + c.Data`
|
|
`Forward()`
| 三条 non-streaming 子路径均填
`responseBody`
;
`ForwardResult.ResponseBody`
填充 |
---
## 六、依赖注入
**`backend/cmd/server/wire_gen.go`**
(已生成,确认包含)
```
go
requestCaptureLogRepository
:=
repository
.
NewRequestCaptureLogRepository
(
client
)
requestCaptureService
:=
service
.
NewRequestCaptureService
(
requestCaptureLogRepository
,
configConfig
)
gatewayHandler
:=
handler
.
NewGatewayHandler
(
...
,
requestCaptureService
,
...
)
openAIGatewayHandler
:=
handler
.
NewOpenAIGatewayHandler
(
...
,
requestCaptureService
,
...
)
```
---
## 七、端点覆盖总览
| 端点 | 协议 | 请求捕获 | 响应捕获 |
|---|---|---|---|
|
`POST /v1/messages`
→ Anthropic 账号 | Claude | ✅ | ✅ |
|
`POST /v1/messages`
→ CC 转发 | Claude→OpenAI CC | ✅ | ✅ |
|
`POST /v1/messages`
→ Antigravity 账号 | Claude→Gemini | ✅ | ✅ |
|
`POST /v1/messages`
→ GeminiCompat 账号 | Claude→Gemini | ✅ | ✅ |
|
`POST /openai/v1/chat/completions`
| OpenAI CC | ✅ | ✅ |
|
`POST /openai/v1/responses`
(Codex path) | OpenAI Responses | ✅ | ✅ |
|
`POST /openai/v1/responses`
(Messages path) | OpenAI→Anthropic | ✅ | ✅ |
---
## 八、测试流程
### 8.1 前置条件
```
bash
# 1. 执行数据库迁移
psql
-U
sub2api
-d
sub2api
-f
backend/migrations/108_request_capture_log.sql
# 2. 配置(选其一)
# 方式A:config.yaml 已有 request_capture 块,nfs_path 留空则只写 DB
# 方式B:环境变量
export
REQUEST_CAPTURE_NFS_PATH
=
/tmp/nfs_test/
# 留空则跳过 NFS
# 3. 给测试用 API Key 开启采集标志
psql
-U
sub2api
-d
sub2api
-c
\
"UPDATE api_keys SET capture_requests = true WHERE id = <your_key_id>;"
```
### 8.2 测试矩阵
每个端点各跑一次
**非流式**
和
**流式**
请求。
```
bash
KEY
=
"your-capture-enabled-key"
BASE
=
"http://localhost:8080"
# ── Claude 端点(非流式)──
curl
-s
-X
POST
$BASE
/v1/messages
\
-H
"x-api-key:
$KEY
"
-H
"Content-Type: application/json"
\
-d
'{"model":"claude-3-5-haiku-20241022","max_tokens":50,
"messages":[{"role":"user","content":"say hi"}]}'
# ── Claude 端点(流式)──
curl
-s
-X
POST
$BASE
/v1/messages
\
-H
"x-api-key:
$KEY
"
-H
"Content-Type: application/json"
\
-d
'{"model":"claude-3-5-haiku-20241022","max_tokens":50,"stream":true,
"messages":[{"role":"user","content":"say hi"}]}'
# ── OpenAI Responses 端点 ──
curl
-s
-X
POST
$BASE
/openai/v1/responses
\
-H
"Authorization: Bearer
$KEY
"
-H
"Content-Type: application/json"
\
-d
'{"model":"gpt-4o","input":[{"role":"user","content":"say hi"}]}'
# ── OpenAI Chat Completions 端点 ──
curl
-s
-X
POST
$BASE
/openai/v1/chat/completions
\
-H
"Authorization: Bearer
$KEY
"
-H
"Content-Type: application/json"
\
-d
'{"model":"gpt-4o","messages":[{"role":"user","content":"say hi"}]}'
```
### 8.3 DB 验证
```
sql
-- ① 请求后立即查(DB 写入是同步的,应立即有记录)
SELECT
id
,
api_key_id
,
path
,
method
,
(
request_body
IS
NOT
NULL
)
AS
has_req
,
(
response_body
IS
NOT
NULL
)
AS
has_resp
,
nfs_file_path
,
created_at
FROM
request_capture_logs
ORDER
BY
created_at
DESC
LIMIT
10
;
-- ② 等待 ~1s 后查响应体(CaptureResponse 是异步的)
SELECT
id
,
length
(
request_body
)
AS
req_len
,
length
(
response_body
)
AS
resp_len
,
left
(
response_body
,
100
)
AS
resp_preview
FROM
request_capture_logs
ORDER
BY
id
DESC
LIMIT
5
;
```
**预期结果:**
| 场景 | has_req | has_resp | resp_preview |
|---|---|---|---|
| 非流式请求 |
`true`
|
`true`
(~1s 内) | JSON,以
`{`
开头 |
| 流式请求 |
`true`
|
`true`
(流结束后) | 纯文本,如
`"Hi! How can I..."`
|
| 未开启 capture_requests 的 Key | 无记录 | — | — |
### 8.4 NFS 验证(配置了 `nfs_path` 时)
```
bash
DATE
=
$(
date
+%Y-%m-%d
)
API_KEY_ID
=
<your_key_id>
NFS_DIR
=
"
${
REQUEST_CAPTURE_NFS_PATH
}
/
${
DATE
}
/
${
API_KEY_ID
}
"
# 查看文件列表(应有请求文件和响应文件成对出现)
ls
-la
"
$NFS_DIR
/"
# 查看请求文件结构
cat
"
$NFS_DIR
/"
*
.json | python3
-m
json.tool |
head
-20
# 查看响应文件结构(_response 后缀)
cat
"
$NFS_DIR
/"
*
_response.json | python3
-m
json.tool |
head
-20
```
**预期文件结构:**
```
json
//
请求文件:
{
unixNano
}
_
{
requestID
}
.json
{
"api_key_id"
:
42
,
"user_id"
:
1
,
"request_id"
:
"req-xxx"
,
"created_at"
:
"2024-01-01T00:00:00Z"
,
"path"
:
"/v1/messages"
,
"method"
:
"POST"
,
"ip_address"
:
"127.0.0.1"
,
"body"
:
{
"model"
:
"..."
,
"messages"
:
[
...
]
}
}
//
响应文件:
{
unixNano
}
_
{
requestID
}
_response.json
{
"capture_id"
:
123
,
"created_at"
:
"2024-01-01T00:00:01Z"
,
"body"
:
{
"id"
:
"msg_xxx"
,
"content"
:
[
...
]
}
//
非流式为完整JSON;流式为纯文本字符串
}
```
### 8.5 对照组(负向验证)
```
bash
NO_CAPTURE_KEY
=
"your-normal-key"
curl
-s
-X
POST
$BASE
/v1/messages
\
-H
"x-api-key:
$NO_CAPTURE_KEY
"
-H
"Content-Type: application/json"
\
-d
'{"model":"claude-3-5-haiku-20241022","max_tokens":10,
"messages":[{"role":"user","content":"hi"}]}'
```
```
sql
-- 该 key 对应的 api_key_id 不应有任何记录
SELECT
count
(
*
)
FROM
request_capture_logs
WHERE
api_key_id
=
<
no_capture_key_id
>
;
-- 预期:0
```
### 8.6 异常场景
| 场景 | 预期行为 |
|---|---|
| DB 写 request 失败 |
`captureID = 0`
,后续
`CaptureResponse`
自动跳过,请求正常返回 |
| DB 写 response 失败 | 记录 error 日志,请求已正常返回,不影响用户 |
| NFS 目录不存在 |
`MkdirAll`
自动创建;失败则 error 日志,不影响 DB 写入 |
| 流式请求客户端中途断开 | buffer 内已采集的文本会被写入,响应体为截断内容(属预期行为)|
|
`captureID`
泄漏(CaptureResponse 从未被调用)|
`sync.Map`
中的条目会滞留,但量级等同于并发请求数,可忽略 |
backend/cmd/jwtgen/main.go
View file @
3b7a5fff
...
@@ -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
(
client
,
userRepo
,
nil
,
nil
,
cfg
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
authService
:=
service
.
NewAuthService
(
client
,
userRepo
,
nil
,
nil
,
cfg
,
nil
,
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
()
...
...
backend/cmd/server/VERSION
View file @
3b7a5fff
0.1.11
7
0.1.11
9
backend/cmd/server/wire_gen.go
View file @
3b7a5fff
...
@@ -69,7 +69,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -69,7 +69,9 @@ 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
)
subscriptionService
:=
service
.
NewSubscriptionService
(
groupRepository
,
userSubscriptionRepository
,
billingCacheService
,
client
,
configConfig
)
subscriptionService
:=
service
.
NewSubscriptionService
(
groupRepository
,
userSubscriptionRepository
,
billingCacheService
,
client
,
configConfig
)
authService
:=
service
.
NewAuthService
(
client
,
userRepository
,
redeemCodeRepository
,
refreshTokenCache
,
configConfig
,
settingService
,
emailService
,
turnstileService
,
emailQueueService
,
promoService
,
subscriptionService
)
affiliateRepository
:=
repository
.
NewAffiliateRepository
(
client
,
db
)
affiliateService
:=
service
.
NewAffiliateService
(
affiliateRepository
,
settingService
,
apiKeyAuthCacheInvalidator
,
billingCacheService
)
authService
:=
service
.
NewAuthService
(
client
,
userRepository
,
redeemCodeRepository
,
refreshTokenCache
,
configConfig
,
settingService
,
emailService
,
turnstileService
,
emailQueueService
,
promoService
,
subscriptionService
,
affiliateService
)
userService
:=
service
.
NewUserService
(
userRepository
,
settingRepository
,
apiKeyAuthCacheInvalidator
,
billingCache
)
userService
:=
service
.
NewUserService
(
userRepository
,
settingRepository
,
apiKeyAuthCacheInvalidator
,
billingCache
)
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
)
...
@@ -80,7 +82,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -80,7 +82,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
totpCache
:=
repository
.
NewTotpCache
(
redisClient
)
totpCache
:=
repository
.
NewTotpCache
(
redisClient
)
totpService
:=
service
.
NewTotpService
(
userRepository
,
secretEncryptor
,
totpCache
,
settingService
,
emailService
,
emailQueueService
)
totpService
:=
service
.
NewTotpService
(
userRepository
,
secretEncryptor
,
totpCache
,
settingService
,
emailService
,
emailQueueService
)
authHandler
:=
handler
.
NewAuthHandler
(
configConfig
,
authService
,
userService
,
settingService
,
promoService
,
redeemService
,
totpService
)
authHandler
:=
handler
.
NewAuthHandler
(
configConfig
,
authService
,
userService
,
settingService
,
promoService
,
redeemService
,
totpService
)
userHandler
:=
handler
.
NewUserHandler
(
userService
,
authService
,
emailService
,
emailCache
)
userHandler
:=
handler
.
NewUserHandler
(
userService
,
authService
,
emailService
,
emailCache
,
affiliateService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
apiKeyHandler
:=
handler
.
NewAPIKeyHandler
(
apiKeyService
)
usageLogRepository
:=
repository
.
NewUsageLogRepository
(
client
,
db
)
usageLogRepository
:=
repository
.
NewUsageLogRepository
(
client
,
db
)
usageService
:=
service
.
NewUsageService
(
usageLogRepository
,
userRepository
,
client
,
apiKeyAuthCacheInvalidator
)
usageService
:=
service
.
NewUsageService
(
usageLogRepository
,
userRepository
,
client
,
apiKeyAuthCacheInvalidator
)
...
@@ -91,6 +93,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -91,6 +93,9 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
announcementReadRepository
:=
repository
.
NewAnnouncementReadRepository
(
client
)
announcementReadRepository
:=
repository
.
NewAnnouncementReadRepository
(
client
)
announcementService
:=
service
.
NewAnnouncementService
(
announcementRepository
,
announcementReadRepository
,
userRepository
,
userSubscriptionRepository
)
announcementService
:=
service
.
NewAnnouncementService
(
announcementRepository
,
announcementReadRepository
,
userRepository
,
userSubscriptionRepository
)
announcementHandler
:=
handler
.
NewAnnouncementHandler
(
announcementService
)
announcementHandler
:=
handler
.
NewAnnouncementHandler
(
announcementService
)
channelMonitorRepository
:=
repository
.
NewChannelMonitorRepository
(
client
,
db
)
channelMonitorService
:=
service
.
ProvideChannelMonitorService
(
channelMonitorRepository
,
secretEncryptor
)
channelMonitorUserHandler
:=
handler
.
NewChannelMonitorUserHandler
(
channelMonitorService
,
settingService
)
dashboardAggregationRepository
:=
repository
.
NewDashboardAggregationRepository
(
db
)
dashboardAggregationRepository
:=
repository
.
NewDashboardAggregationRepository
(
db
)
dashboardStatsCache
:=
repository
.
NewDashboardCache
(
redisClient
,
configConfig
)
dashboardStatsCache
:=
repository
.
NewDashboardCache
(
redisClient
,
configConfig
)
dashboardService
:=
service
.
NewDashboardService
(
usageLogRepository
,
dashboardAggregationRepository
,
dashboardStatsCache
,
configConfig
)
dashboardService
:=
service
.
NewDashboardService
(
usageLogRepository
,
dashboardAggregationRepository
,
dashboardStatsCache
,
configConfig
)
...
@@ -192,7 +197,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -192,7 +197,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
paymentConfigService
:=
service
.
ProvidePaymentConfigService
(
client
,
settingRepository
,
encryptionKey
)
paymentConfigService
:=
service
.
ProvidePaymentConfigService
(
client
,
settingRepository
,
encryptionKey
)
registry
:=
payment
.
ProvideRegistry
()
registry
:=
payment
.
ProvideRegistry
()
defaultLoadBalancer
:=
payment
.
ProvideDefaultLoadBalancer
(
client
,
encryptionKey
)
defaultLoadBalancer
:=
payment
.
ProvideDefaultLoadBalancer
(
client
,
encryptionKey
)
paymentService
:=
service
.
NewPaymentService
(
client
,
registry
,
defaultLoadBalancer
,
redeemService
,
subscriptionService
,
paymentConfigService
,
userRepository
,
groupRepository
)
paymentService
:=
service
.
NewPaymentService
(
client
,
registry
,
defaultLoadBalancer
,
redeemService
,
subscriptionService
,
paymentConfigService
,
userRepository
,
groupRepository
,
affiliateService
)
settingHandler
:=
admin
.
NewSettingHandler
(
settingService
,
emailService
,
turnstileService
,
opsService
,
paymentConfigService
,
paymentService
)
settingHandler
:=
admin
.
NewSettingHandler
(
settingService
,
emailService
,
turnstileService
,
opsService
,
paymentConfigService
,
paymentService
)
opsHandler
:=
admin
.
NewOpsHandler
(
opsService
)
opsHandler
:=
admin
.
NewOpsHandler
(
opsService
)
updateCache
:=
repository
.
NewUpdateCache
(
redisClient
)
updateCache
:=
repository
.
NewUpdateCache
(
redisClient
)
...
@@ -221,21 +226,13 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -221,21 +226,13 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
scheduledTestService
:=
service
.
ProvideScheduledTestService
(
scheduledTestPlanRepository
,
scheduledTestResultRepository
)
scheduledTestService
:=
service
.
ProvideScheduledTestService
(
scheduledTestPlanRepository
,
scheduledTestResultRepository
)
scheduledTestHandler
:=
admin
.
NewScheduledTestHandler
(
scheduledTestService
)
scheduledTestHandler
:=
admin
.
NewScheduledTestHandler
(
scheduledTestService
)
channelHandler
:=
admin
.
NewChannelHandler
(
channelService
,
billingService
)
channelHandler
:=
admin
.
NewChannelHandler
(
channelService
,
billingService
)
sqlDB
,
err
:=
repository
.
ProvideSQLDB
(
client
)
channelMonitorHandler
:=
admin
.
NewChannelMonitorHandler
(
channelMonitorService
)
if
err
!=
nil
{
channelMonitorRequestTemplateRepository
:=
repository
.
NewChannelMonitorRequestTemplateRepository
(
client
,
db
)
return
nil
,
err
}
channelMonitorRepository
:=
repository
.
NewChannelMonitorRepository
(
client
,
sqlDB
)
channelMonitorRequestTemplateRepository
:=
repository
.
NewChannelMonitorRequestTemplateRepository
(
client
,
sqlDB
)
channelMonitorRequestTemplateService
:=
service
.
NewChannelMonitorRequestTemplateService
(
channelMonitorRequestTemplateRepository
)
channelMonitorRequestTemplateService
:=
service
.
NewChannelMonitorRequestTemplateService
(
channelMonitorRequestTemplateRepository
)
channelMonitorRequestTemplateHandler
:=
admin
.
NewChannelMonitorRequestTemplateHandler
(
channelMonitorRequestTemplateService
)
channelMonitorRequestTemplateHandler
:=
admin
.
NewChannelMonitorRequestTemplateHandler
(
channelMonitorRequestTemplateService
)
channelMonitorService
:=
service
.
ProvideChannelMonitorService
(
channelMonitorRepository
,
secretEncryptor
)
channelMonitorHandler
:=
admin
.
NewChannelMonitorHandler
(
channelMonitorService
)
channelMonitorUserHandler
:=
handler
.
NewChannelMonitorUserHandler
(
channelMonitorService
,
settingService
)
channelMonitorRunner
:=
service
.
ProvideChannelMonitorRunner
(
channelMonitorService
,
settingService
)
paymentHandler
:=
admin
.
NewPaymentHandler
(
paymentService
,
paymentConfigService
)
paymentHandler
:=
admin
.
NewPaymentHandler
(
paymentService
,
paymentConfigService
)
a
vailableChannelUserHandler
:=
handler
.
NewAvailableChannelHandler
(
channelService
,
apiKey
Service
,
sett
in
g
Service
)
a
ffiliateHandler
:=
admin
.
NewAffiliateHandler
(
affiliate
Service
,
adm
inService
)
adminHandlers
:=
handler
.
ProvideAdminHandlers
(
dashboardHandler
,
adminUserHandler
,
groupHandler
,
accountHandler
,
adminAnnouncementHandler
,
dataManagementHandler
,
backupHandler
,
oAuthHandler
,
openAIOAuthHandler
,
geminiOAuthHandler
,
antigravityOAuthHandler
,
proxyHandler
,
adminRedeemHandler
,
promoHandler
,
settingHandler
,
opsHandler
,
systemHandler
,
adminSubscriptionHandler
,
adminUsageHandler
,
userAttributeHandler
,
errorPassthroughHandler
,
tlsFingerprintProfileHandler
,
adminAPIKeyHandler
,
scheduledTestHandler
,
channelHandler
,
channelMonitorHandler
,
channelMonitorRequestTemplateHandler
,
paymentHandler
)
adminHandlers
:=
handler
.
ProvideAdminHandlers
(
dashboardHandler
,
adminUserHandler
,
groupHandler
,
accountHandler
,
adminAnnouncementHandler
,
dataManagementHandler
,
backupHandler
,
oAuthHandler
,
openAIOAuthHandler
,
geminiOAuthHandler
,
antigravityOAuthHandler
,
proxyHandler
,
adminRedeemHandler
,
promoHandler
,
settingHandler
,
opsHandler
,
systemHandler
,
adminSubscriptionHandler
,
adminUsageHandler
,
userAttributeHandler
,
errorPassthroughHandler
,
tlsFingerprintProfileHandler
,
adminAPIKeyHandler
,
scheduledTestHandler
,
channelHandler
,
channelMonitorHandler
,
channelMonitorRequestTemplateHandler
,
paymentHandler
,
affiliateHandler
)
usageRecordWorkerPool
:=
service
.
NewUsageRecordWorkerPool
(
configConfig
)
usageRecordWorkerPool
:=
service
.
NewUsageRecordWorkerPool
(
configConfig
)
requestCaptureLogRepository
:=
repository
.
NewRequestCaptureLogRepository
(
client
)
requestCaptureLogRepository
:=
repository
.
NewRequestCaptureLogRepository
(
client
)
requestCaptureService
:=
service
.
NewRequestCaptureService
(
requestCaptureLogRepository
,
configConfig
)
requestCaptureService
:=
service
.
NewRequestCaptureService
(
requestCaptureLogRepository
,
configConfig
)
...
@@ -247,9 +244,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -247,9 +244,10 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
totpHandler
:=
handler
.
NewTotpHandler
(
totpService
)
totpHandler
:=
handler
.
NewTotpHandler
(
totpService
)
handlerPaymentHandler
:=
handler
.
NewPaymentHandler
(
paymentService
,
paymentConfigService
,
channelService
)
handlerPaymentHandler
:=
handler
.
NewPaymentHandler
(
paymentService
,
paymentConfigService
,
channelService
)
paymentWebhookHandler
:=
handler
.
NewPaymentWebhookHandler
(
paymentService
,
registry
)
paymentWebhookHandler
:=
handler
.
NewPaymentWebhookHandler
(
paymentService
,
registry
)
availableChannelHandler
:=
handler
.
NewAvailableChannelHandler
(
channelService
,
apiKeyService
,
settingService
)
idempotencyCoordinator
:=
service
.
ProvideIdempotencyCoordinator
(
idempotencyRepository
,
configConfig
)
idempotencyCoordinator
:=
service
.
ProvideIdempotencyCoordinator
(
idempotencyRepository
,
configConfig
)
idempotencyCleanupService
:=
service
.
ProvideIdempotencyCleanupService
(
idempotencyRepository
,
configConfig
)
idempotencyCleanupService
:=
service
.
ProvideIdempotencyCleanupService
(
idempotencyRepository
,
configConfig
)
handlers
:=
handler
.
ProvideHandlers
(
authHandler
,
userHandler
,
apiKeyHandler
,
usageHandler
,
redeemHandler
,
subscriptionHandler
,
announcementHandler
,
channelMonitorUserHandler
,
adminHandlers
,
gatewayHandler
,
openAIGatewayHandler
,
handlerSettingHandler
,
totpHandler
,
handlerPaymentHandler
,
paymentWebhookHandler
,
availableChannel
User
Handler
,
idempotencyCoordinator
,
idempotencyCleanupService
)
handlers
:=
handler
.
ProvideHandlers
(
authHandler
,
userHandler
,
apiKeyHandler
,
usageHandler
,
redeemHandler
,
subscriptionHandler
,
announcementHandler
,
channelMonitorUserHandler
,
adminHandlers
,
gatewayHandler
,
openAIGatewayHandler
,
handlerSettingHandler
,
totpHandler
,
handlerPaymentHandler
,
paymentWebhookHandler
,
availableChannelHandler
,
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
)
...
@@ -265,6 +263,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
...
@@ -265,6 +263,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
subscriptionExpiryService
:=
service
.
ProvideSubscriptionExpiryService
(
userSubscriptionRepository
)
subscriptionExpiryService
:=
service
.
ProvideSubscriptionExpiryService
(
userSubscriptionRepository
)
scheduledTestRunnerService
:=
service
.
ProvideScheduledTestRunnerService
(
scheduledTestPlanRepository
,
scheduledTestService
,
accountTestService
,
rateLimitService
,
configConfig
)
scheduledTestRunnerService
:=
service
.
ProvideScheduledTestRunnerService
(
scheduledTestPlanRepository
,
scheduledTestService
,
accountTestService
,
rateLimitService
,
configConfig
)
paymentOrderExpiryService
:=
service
.
ProvidePaymentOrderExpiryService
(
paymentService
)
paymentOrderExpiryService
:=
service
.
ProvidePaymentOrderExpiryService
(
paymentService
)
channelMonitorRunner
:=
service
.
ProvideChannelMonitorRunner
(
channelMonitorService
,
settingService
)
v
:=
provideCleanup
(
client
,
redisClient
,
opsMetricsCollector
,
opsAggregationService
,
opsAlertEvaluatorService
,
opsCleanupService
,
opsScheduledReportService
,
opsSystemLogSink
,
schedulerSnapshotService
,
tokenRefreshService
,
accountExpiryService
,
subscriptionExpiryService
,
usageCleanupService
,
idempotencyCleanupService
,
pricingService
,
emailQueueService
,
billingCacheService
,
usageRecordWorkerPool
,
subscriptionService
,
oAuthService
,
openAIOAuthService
,
geminiOAuthService
,
antigravityOAuthService
,
openAIGatewayService
,
scheduledTestRunnerService
,
backupService
,
paymentOrderExpiryService
,
channelMonitorRunner
)
v
:=
provideCleanup
(
client
,
redisClient
,
opsMetricsCollector
,
opsAggregationService
,
opsAlertEvaluatorService
,
opsCleanupService
,
opsScheduledReportService
,
opsSystemLogSink
,
schedulerSnapshotService
,
tokenRefreshService
,
accountExpiryService
,
subscriptionExpiryService
,
usageCleanupService
,
idempotencyCleanupService
,
pricingService
,
emailQueueService
,
billingCacheService
,
usageRecordWorkerPool
,
subscriptionService
,
oAuthService
,
openAIOAuthService
,
geminiOAuthService
,
antigravityOAuthService
,
openAIGatewayService
,
scheduledTestRunnerService
,
backupService
,
paymentOrderExpiryService
,
channelMonitorRunner
)
application
:=
&
Application
{
application
:=
&
Application
{
Server
:
httpServer
,
Server
:
httpServer
,
...
...
backend/internal/handler/admin/account_handler.go
View file @
3b7a5fff
...
@@ -652,6 +652,7 @@ func (h *AccountHandler) Delete(c *gin.Context) {
...
@@ -652,6 +652,7 @@ func (h *AccountHandler) Delete(c *gin.Context) {
type
TestAccountRequest
struct
{
type
TestAccountRequest
struct
{
ModelID
string
`json:"model_id"`
ModelID
string
`json:"model_id"`
Prompt
string
`json:"prompt"`
Prompt
string
`json:"prompt"`
Mode
string
`json:"mode"`
}
}
type
SyncFromCRSRequest
struct
{
type
SyncFromCRSRequest
struct
{
...
@@ -682,7 +683,7 @@ func (h *AccountHandler) Test(c *gin.Context) {
...
@@ -682,7 +683,7 @@ func (h *AccountHandler) Test(c *gin.Context) {
_
=
c
.
ShouldBindJSON
(
&
req
)
_
=
c
.
ShouldBindJSON
(
&
req
)
// Use AccountTestService to test the account with SSE streaming
// Use AccountTestService to test the account with SSE streaming
if
err
:=
h
.
accountTestService
.
TestAccountConnection
(
c
,
accountID
,
req
.
ModelID
,
req
.
Prompt
);
err
!=
nil
{
if
err
:=
h
.
accountTestService
.
TestAccountConnection
(
c
,
accountID
,
req
.
ModelID
,
req
.
Prompt
,
req
.
Mode
);
err
!=
nil
{
// Error already sent via SSE, just log
// Error already sent via SSE, just log
return
return
}
}
...
...
backend/internal/handler/admin/affiliate_handler.go
0 → 100644
View file @
3b7a5fff
package
admin
import
(
"strconv"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// AffiliateHandler handles admin affiliate (邀请返利) management:
// listing users with custom settings, updating per-user invite codes
// and exclusive rebate rates, and batch operations.
type
AffiliateHandler
struct
{
affiliateService
*
service
.
AffiliateService
adminService
service
.
AdminService
}
// NewAffiliateHandler creates a new admin affiliate handler.
func
NewAffiliateHandler
(
affiliateService
*
service
.
AffiliateService
,
adminService
service
.
AdminService
)
*
AffiliateHandler
{
return
&
AffiliateHandler
{
affiliateService
:
affiliateService
,
adminService
:
adminService
,
}
}
// ListUsers returns paginated users with custom affiliate settings.
// GET /api/v1/admin/affiliates/users
func
(
h
*
AffiliateHandler
)
ListUsers
(
c
*
gin
.
Context
)
{
page
,
pageSize
:=
response
.
ParsePagination
(
c
)
search
:=
c
.
Query
(
"search"
)
entries
,
total
,
err
:=
h
.
affiliateService
.
AdminListCustomUsers
(
c
.
Request
.
Context
(),
service
.
AffiliateAdminFilter
{
Search
:
search
,
Page
:
page
,
PageSize
:
pageSize
,
})
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Paginated
(
c
,
entries
,
total
,
page
,
pageSize
)
}
// UpdateUserSettings updates a user's affiliate settings.
// PUT /api/v1/admin/affiliates/users/:user_id
//
// Both fields are optional and applied independently.
type
UpdateAffiliateUserRequest
struct
{
AffCode
*
string
`json:"aff_code"`
AffRebateRatePercent
*
float64
`json:"aff_rebate_rate_percent"`
// ClearRebateRate explicitly clears the per-user rate (sets it to NULL).
// Used to disambiguate from "field not provided".
ClearRebateRate
bool
`json:"clear_rebate_rate"`
}
func
(
h
*
AffiliateHandler
)
UpdateUserSettings
(
c
*
gin
.
Context
)
{
userID
,
err
:=
strconv
.
ParseInt
(
c
.
Param
(
"user_id"
),
10
,
64
)
if
err
!=
nil
||
userID
<=
0
{
response
.
BadRequest
(
c
,
"Invalid user_id"
)
return
}
var
req
UpdateAffiliateUserRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
if
req
.
AffCode
!=
nil
{
if
err
:=
h
.
affiliateService
.
AdminUpdateUserAffCode
(
c
.
Request
.
Context
(),
userID
,
*
req
.
AffCode
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
}
if
req
.
ClearRebateRate
{
if
err
:=
h
.
affiliateService
.
AdminSetUserRebateRate
(
c
.
Request
.
Context
(),
userID
,
nil
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
}
else
if
req
.
AffRebateRatePercent
!=
nil
{
if
err
:=
h
.
affiliateService
.
AdminSetUserRebateRate
(
c
.
Request
.
Context
(),
userID
,
req
.
AffRebateRatePercent
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
}
response
.
Success
(
c
,
gin
.
H
{
"user_id"
:
userID
})
}
// ClearUserSettings removes ALL of a user's custom affiliate settings — clears
// the exclusive rebate rate AND regenerates the invite code as a new system
// random one. Conceptually this "removes the user from the custom list".
//
// Both writes happen in this handler; failure of one leaves the other applied,
// but the operation is idempotent so the admin can re-run it safely.
// DELETE /api/v1/admin/affiliates/users/:user_id
func
(
h
*
AffiliateHandler
)
ClearUserSettings
(
c
*
gin
.
Context
)
{
userID
,
err
:=
strconv
.
ParseInt
(
c
.
Param
(
"user_id"
),
10
,
64
)
if
err
!=
nil
||
userID
<=
0
{
response
.
BadRequest
(
c
,
"Invalid user_id"
)
return
}
if
err
:=
h
.
affiliateService
.
AdminSetUserRebateRate
(
c
.
Request
.
Context
(),
userID
,
nil
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
if
_
,
err
:=
h
.
affiliateService
.
AdminResetUserAffCode
(
c
.
Request
.
Context
(),
userID
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"user_id"
:
userID
})
}
// BatchSetRate applies the same rebate rate (or clears it) to multiple users.
//
// Protocol: pass `clear: true` to clear rates (aff_rebate_rate_percent is
// ignored). Otherwise aff_rebate_rate_percent is required and applied to
// every user_id. The explicit `clear` flag exists because Go's JSON unmarshal
// can't distinguish a missing field from `null`, and a silent clear from a
// frontend that forgot to include the rate would be a footgun.
//
// POST /api/v1/admin/affiliates/users/batch-rate
type
BatchSetRateRequest
struct
{
UserIDs
[]
int64
`json:"user_ids" binding:"required"`
AffRebateRatePercent
*
float64
`json:"aff_rebate_rate_percent"`
Clear
bool
`json:"clear"`
}
func
(
h
*
AffiliateHandler
)
BatchSetRate
(
c
*
gin
.
Context
)
{
var
req
BatchSetRateRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
if
len
(
req
.
UserIDs
)
==
0
{
response
.
BadRequest
(
c
,
"user_ids cannot be empty"
)
return
}
if
!
req
.
Clear
&&
req
.
AffRebateRatePercent
==
nil
{
response
.
BadRequest
(
c
,
"aff_rebate_rate_percent is required unless clear=true"
)
return
}
rate
:=
req
.
AffRebateRatePercent
if
req
.
Clear
{
rate
=
nil
}
if
err
:=
h
.
affiliateService
.
AdminBatchSetUserRebateRate
(
c
.
Request
.
Context
(),
req
.
UserIDs
,
rate
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"affected"
:
len
(
req
.
UserIDs
)})
}
// AffiliateUserSummary is the minimal user shape returned by LookupUsers,
// shared with the frontend's add-custom-user picker.
type
AffiliateUserSummary
struct
{
ID
int64
`json:"id"`
Email
string
`json:"email"`
Username
string
`json:"username"`
}
// LookupUsers searches users by email/username for the "add custom user" modal.
// GET /api/v1/admin/affiliates/users/lookup?q=
func
(
h
*
AffiliateHandler
)
LookupUsers
(
c
*
gin
.
Context
)
{
keyword
:=
c
.
Query
(
"q"
)
if
keyword
==
""
{
response
.
Success
(
c
,
[]
AffiliateUserSummary
{})
return
}
users
,
_
,
err
:=
h
.
adminService
.
ListUsers
(
c
.
Request
.
Context
(),
1
,
20
,
service
.
UserListFilters
{
Search
:
keyword
},
"email"
,
"asc"
)
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
result
:=
make
([]
AffiliateUserSummary
,
len
(
users
))
for
i
,
u
:=
range
users
{
result
[
i
]
=
AffiliateUserSummary
{
ID
:
u
.
ID
,
Email
:
u
.
Email
,
Username
:
u
.
Username
}
}
response
.
Success
(
c
,
result
)
}
backend/internal/handler/admin/setting_handler.go
View file @
3b7a5fff
...
@@ -185,6 +185,10 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
...
@@ -185,6 +185,10 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
settings
.
CustomEndpoints
),
DefaultConcurrency
:
settings
.
DefaultConcurrency
,
DefaultConcurrency
:
settings
.
DefaultConcurrency
,
DefaultBalance
:
settings
.
DefaultBalance
,
DefaultBalance
:
settings
.
DefaultBalance
,
AffiliateRebateRate
:
settings
.
AffiliateRebateRate
,
AffiliateRebateFreezeHours
:
settings
.
AffiliateRebateFreezeHours
,
AffiliateRebateDurationDays
:
settings
.
AffiliateRebateDurationDays
,
AffiliateRebatePerInviteeCap
:
settings
.
AffiliateRebatePerInviteeCap
,
DefaultUserRPMLimit
:
settings
.
DefaultUserRPMLimit
,
DefaultUserRPMLimit
:
settings
.
DefaultUserRPMLimit
,
DefaultSubscriptions
:
defaultSubscriptions
,
DefaultSubscriptions
:
defaultSubscriptions
,
EnableModelFallback
:
settings
.
EnableModelFallback
,
EnableModelFallback
:
settings
.
EnableModelFallback
,
...
@@ -241,6 +245,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
...
@@ -241,6 +245,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
ChannelMonitorDefaultIntervalSeconds
:
settings
.
ChannelMonitorDefaultIntervalSeconds
,
ChannelMonitorDefaultIntervalSeconds
:
settings
.
ChannelMonitorDefaultIntervalSeconds
,
AvailableChannelsEnabled
:
settings
.
AvailableChannelsEnabled
,
AvailableChannelsEnabled
:
settings
.
AvailableChannelsEnabled
,
AffiliateEnabled
:
settings
.
AffiliateEnabled
,
}
}
response
.
Success
(
c
,
systemSettingsResponseData
(
payload
,
authSourceDefaults
))
response
.
Success
(
c
,
systemSettingsResponseData
(
payload
,
authSourceDefaults
))
}
}
...
@@ -338,6 +344,10 @@ type UpdateSettingsRequest struct {
...
@@ -338,6 +344,10 @@ type UpdateSettingsRequest struct {
// 默认配置
// 默认配置
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultBalance
float64
`json:"default_balance"`
DefaultBalance
float64
`json:"default_balance"`
AffiliateRebateRate
*
float64
`json:"affiliate_rebate_rate"`
AffiliateRebateFreezeHours
*
int
`json:"affiliate_rebate_freeze_hours"`
AffiliateRebateDurationDays
*
int
`json:"affiliate_rebate_duration_days"`
AffiliateRebatePerInviteeCap
*
float64
`json:"affiliate_rebate_per_invitee_cap"`
DefaultUserRPMLimit
int
`json:"default_user_rpm_limit"`
DefaultUserRPMLimit
int
`json:"default_user_rpm_limit"`
DefaultSubscriptions
[]
dto
.
DefaultSubscriptionSetting
`json:"default_subscriptions"`
DefaultSubscriptions
[]
dto
.
DefaultSubscriptionSetting
`json:"default_subscriptions"`
AuthSourceDefaultEmailBalance
*
float64
`json:"auth_source_default_email_balance"`
AuthSourceDefaultEmailBalance
*
float64
`json:"auth_source_default_email_balance"`
...
@@ -439,6 +449,9 @@ type UpdateSettingsRequest struct {
...
@@ -439,6 +449,9 @@ type UpdateSettingsRequest struct {
// Available Channels feature switch (user-facing)
// Available Channels feature switch (user-facing)
AvailableChannelsEnabled
*
bool
`json:"available_channels_enabled"`
AvailableChannelsEnabled
*
bool
`json:"available_channels_enabled"`
// Affiliate (邀请返利) feature switch
AffiliateEnabled
*
bool
`json:"affiliate_enabled"`
}
}
// UpdateSettings 更新系统设置
// UpdateSettings 更新系统设置
...
@@ -468,6 +481,43 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -468,6 +481,43 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
if
req
.
DefaultBalance
<
0
{
if
req
.
DefaultBalance
<
0
{
req
.
DefaultBalance
=
0
req
.
DefaultBalance
=
0
}
}
affiliateRebateRate
:=
previousSettings
.
AffiliateRebateRate
if
req
.
AffiliateRebateRate
!=
nil
{
affiliateRebateRate
=
*
req
.
AffiliateRebateRate
}
if
affiliateRebateRate
<
service
.
AffiliateRebateRateMin
{
affiliateRebateRate
=
service
.
AffiliateRebateRateMin
}
if
affiliateRebateRate
>
service
.
AffiliateRebateRateMax
{
affiliateRebateRate
=
service
.
AffiliateRebateRateMax
}
affiliateRebateFreezeHours
:=
previousSettings
.
AffiliateRebateFreezeHours
if
req
.
AffiliateRebateFreezeHours
!=
nil
{
affiliateRebateFreezeHours
=
*
req
.
AffiliateRebateFreezeHours
}
if
affiliateRebateFreezeHours
<
0
{
affiliateRebateFreezeHours
=
service
.
AffiliateRebateFreezeHoursDefault
}
if
affiliateRebateFreezeHours
>
service
.
AffiliateRebateFreezeHoursMax
{
affiliateRebateFreezeHours
=
service
.
AffiliateRebateFreezeHoursMax
}
affiliateRebateDurationDays
:=
previousSettings
.
AffiliateRebateDurationDays
if
req
.
AffiliateRebateDurationDays
!=
nil
{
affiliateRebateDurationDays
=
*
req
.
AffiliateRebateDurationDays
}
if
affiliateRebateDurationDays
<
0
{
affiliateRebateDurationDays
=
service
.
AffiliateRebateDurationDaysDefault
}
if
affiliateRebateDurationDays
>
service
.
AffiliateRebateDurationDaysMax
{
affiliateRebateDurationDays
=
service
.
AffiliateRebateDurationDaysMax
}
affiliateRebatePerInviteeCap
:=
previousSettings
.
AffiliateRebatePerInviteeCap
if
req
.
AffiliateRebatePerInviteeCap
!=
nil
{
affiliateRebatePerInviteeCap
=
*
req
.
AffiliateRebatePerInviteeCap
}
if
affiliateRebatePerInviteeCap
<
0
{
affiliateRebatePerInviteeCap
=
service
.
AffiliateRebatePerInviteeCapDefault
}
// 通用表格配置:兼容旧客户端未传字段时保留当前值。
// 通用表格配置:兼容旧客户端未传字段时保留当前值。
if
req
.
TableDefaultPageSize
<=
0
{
if
req
.
TableDefaultPageSize
<=
0
{
req
.
TableDefaultPageSize
=
previousSettings
.
TableDefaultPageSize
req
.
TableDefaultPageSize
=
previousSettings
.
TableDefaultPageSize
...
@@ -1119,6 +1169,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -1119,6 +1169,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
CustomEndpoints
:
customEndpointsJSON
,
CustomEndpoints
:
customEndpointsJSON
,
DefaultConcurrency
:
req
.
DefaultConcurrency
,
DefaultConcurrency
:
req
.
DefaultConcurrency
,
DefaultBalance
:
req
.
DefaultBalance
,
DefaultBalance
:
req
.
DefaultBalance
,
AffiliateRebateRate
:
affiliateRebateRate
,
AffiliateRebateFreezeHours
:
affiliateRebateFreezeHours
,
AffiliateRebateDurationDays
:
affiliateRebateDurationDays
,
AffiliateRebatePerInviteeCap
:
affiliateRebatePerInviteeCap
,
DefaultUserRPMLimit
:
req
.
DefaultUserRPMLimit
,
DefaultUserRPMLimit
:
req
.
DefaultUserRPMLimit
,
DefaultSubscriptions
:
defaultSubscriptions
,
DefaultSubscriptions
:
defaultSubscriptions
,
EnableModelFallback
:
req
.
EnableModelFallback
,
EnableModelFallback
:
req
.
EnableModelFallback
,
...
@@ -1252,6 +1306,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -1252,6 +1306,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
}
return
previousSettings
.
AvailableChannelsEnabled
return
previousSettings
.
AvailableChannelsEnabled
}(),
}(),
AffiliateEnabled
:
func
()
bool
{
if
req
.
AffiliateEnabled
!=
nil
{
return
*
req
.
AffiliateEnabled
}
return
previousSettings
.
AffiliateEnabled
}(),
}
}
authSourceDefaults
:=
&
service
.
AuthSourceDefaultSettings
{
authSourceDefaults
:=
&
service
.
AuthSourceDefaultSettings
{
...
@@ -1433,6 +1493,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -1433,6 +1493,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
updatedSettings
.
CustomEndpoints
),
CustomEndpoints
:
dto
.
ParseCustomEndpoints
(
updatedSettings
.
CustomEndpoints
),
DefaultConcurrency
:
updatedSettings
.
DefaultConcurrency
,
DefaultConcurrency
:
updatedSettings
.
DefaultConcurrency
,
DefaultBalance
:
updatedSettings
.
DefaultBalance
,
DefaultBalance
:
updatedSettings
.
DefaultBalance
,
AffiliateRebateRate
:
updatedSettings
.
AffiliateRebateRate
,
AffiliateRebateFreezeHours
:
updatedSettings
.
AffiliateRebateFreezeHours
,
AffiliateRebateDurationDays
:
updatedSettings
.
AffiliateRebateDurationDays
,
AffiliateRebatePerInviteeCap
:
updatedSettings
.
AffiliateRebatePerInviteeCap
,
DefaultUserRPMLimit
:
updatedSettings
.
DefaultUserRPMLimit
,
DefaultUserRPMLimit
:
updatedSettings
.
DefaultUserRPMLimit
,
DefaultSubscriptions
:
updatedDefaultSubscriptions
,
DefaultSubscriptions
:
updatedDefaultSubscriptions
,
EnableModelFallback
:
updatedSettings
.
EnableModelFallback
,
EnableModelFallback
:
updatedSettings
.
EnableModelFallback
,
...
@@ -1488,6 +1552,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
...
@@ -1488,6 +1552,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
ChannelMonitorDefaultIntervalSeconds
:
updatedSettings
.
ChannelMonitorDefaultIntervalSeconds
,
ChannelMonitorDefaultIntervalSeconds
:
updatedSettings
.
ChannelMonitorDefaultIntervalSeconds
,
AvailableChannelsEnabled
:
updatedSettings
.
AvailableChannelsEnabled
,
AvailableChannelsEnabled
:
updatedSettings
.
AvailableChannelsEnabled
,
AffiliateEnabled
:
updatedSettings
.
AffiliateEnabled
,
}
}
response
.
Success
(
c
,
systemSettingsResponseData
(
payload
,
updatedAuthSourceDefaults
))
response
.
Success
(
c
,
systemSettingsResponseData
(
payload
,
updatedAuthSourceDefaults
))
}
}
...
@@ -1738,6 +1804,18 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
...
@@ -1738,6 +1804,18 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
before
.
DefaultBalance
!=
after
.
DefaultBalance
{
if
before
.
DefaultBalance
!=
after
.
DefaultBalance
{
changed
=
append
(
changed
,
"default_balance"
)
changed
=
append
(
changed
,
"default_balance"
)
}
}
if
before
.
AffiliateRebateRate
!=
after
.
AffiliateRebateRate
{
changed
=
append
(
changed
,
"affiliate_rebate_rate"
)
}
if
before
.
AffiliateRebateFreezeHours
!=
after
.
AffiliateRebateFreezeHours
{
changed
=
append
(
changed
,
"affiliate_rebate_freeze_hours"
)
}
if
before
.
AffiliateRebateDurationDays
!=
after
.
AffiliateRebateDurationDays
{
changed
=
append
(
changed
,
"affiliate_rebate_duration_days"
)
}
if
before
.
AffiliateRebatePerInviteeCap
!=
after
.
AffiliateRebatePerInviteeCap
{
changed
=
append
(
changed
,
"affiliate_rebate_per_invitee_cap"
)
}
if
!
equalDefaultSubscriptions
(
before
.
DefaultSubscriptions
,
after
.
DefaultSubscriptions
)
{
if
!
equalDefaultSubscriptions
(
before
.
DefaultSubscriptions
,
after
.
DefaultSubscriptions
)
{
changed
=
append
(
changed
,
"default_subscriptions"
)
changed
=
append
(
changed
,
"default_subscriptions"
)
}
}
...
@@ -1853,6 +1931,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
...
@@ -1853,6 +1931,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if
before
.
AvailableChannelsEnabled
!=
after
.
AvailableChannelsEnabled
{
if
before
.
AvailableChannelsEnabled
!=
after
.
AvailableChannelsEnabled
{
changed
=
append
(
changed
,
"available_channels_enabled"
)
changed
=
append
(
changed
,
"available_channels_enabled"
)
}
}
if
before
.
AffiliateEnabled
!=
after
.
AffiliateEnabled
{
changed
=
append
(
changed
,
"affiliate_enabled"
)
}
changed
=
appendAuthSourceDefaultChanges
(
changed
,
beforeAuthSourceDefaults
,
afterAuthSourceDefaults
)
changed
=
appendAuthSourceDefaultChanges
(
changed
,
beforeAuthSourceDefaults
,
afterAuthSourceDefaults
)
return
changed
return
changed
}
}
...
...
backend/internal/handler/auth_handler.go
View file @
3b7a5fff
...
@@ -48,6 +48,7 @@ type RegisterRequest struct {
...
@@ -48,6 +48,7 @@ type RegisterRequest struct {
TurnstileToken
string
`json:"turnstile_token"`
TurnstileToken
string
`json:"turnstile_token"`
PromoCode
string
`json:"promo_code"`
// 注册优惠码
PromoCode
string
`json:"promo_code"`
// 注册优惠码
InvitationCode
string
`json:"invitation_code"`
// 邀请码
InvitationCode
string
`json:"invitation_code"`
// 邀请码
AffCode
string
`json:"aff_code"`
// 邀请返利码
}
}
// SendVerifyCodeRequest 发送验证码请求
// SendVerifyCodeRequest 发送验证码请求
...
@@ -164,7 +165,15 @@ func (h *AuthHandler) Register(c *gin.Context) {
...
@@ -164,7 +165,15 @@ func (h *AuthHandler) Register(c *gin.Context) {
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
,
req
.
AffCode
,
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
...
...
backend/internal/handler/auth_linuxdo_oauth.go
View file @
3b7a5fff
...
@@ -435,6 +435,7 @@ func (h *AuthHandler) createLinuxDoOAuthChoicePendingSession(
...
@@ -435,6 +435,7 @@ func (h *AuthHandler) createLinuxDoOAuthChoicePendingSession(
type
completeLinuxDoOAuthRequest
struct
{
type
completeLinuxDoOAuthRequest
struct
{
InvitationCode
string
`json:"invitation_code" binding:"required"`
InvitationCode
string
`json:"invitation_code" binding:"required"`
AffCode
string
`json:"aff_code,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptAvatar
*
bool
`json:"adopt_avatar,omitempty"`
AdoptAvatar
*
bool
`json:"adopt_avatar,omitempty"`
}
}
...
@@ -518,7 +519,7 @@ func (h *AuthHandler) CompleteLinuxDoOAuthRegistration(c *gin.Context) {
...
@@ -518,7 +519,7 @@ func (h *AuthHandler) CompleteLinuxDoOAuthRegistration(c *gin.Context) {
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
}
}
tokenPair
,
user
,
err
:=
h
.
authService
.
LoginOrRegisterOAuthWithTokenPair
(
c
.
Request
.
Context
(),
email
,
username
,
req
.
InvitationCode
)
tokenPair
,
user
,
err
:=
h
.
authService
.
LoginOrRegisterOAuthWithTokenPair
(
c
.
Request
.
Context
(),
email
,
username
,
req
.
InvitationCode
,
req
.
AffCode
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
...
...
backend/internal/handler/auth_oauth_pending_flow.go
View file @
3b7a5fff
...
@@ -67,6 +67,7 @@ type createPendingOAuthAccountRequest struct {
...
@@ -67,6 +67,7 @@ type createPendingOAuthAccountRequest struct {
VerifyCode
string
`json:"verify_code,omitempty"`
VerifyCode
string
`json:"verify_code,omitempty"`
Password
string
`json:"password" binding:"required,min=6"`
Password
string
`json:"password" binding:"required,min=6"`
InvitationCode
string
`json:"invitation_code,omitempty"`
InvitationCode
string
`json:"invitation_code,omitempty"`
AffCode
string
`json:"aff_code,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptAvatar
*
bool
`json:"adopt_avatar,omitempty"`
AdoptAvatar
*
bool
`json:"adopt_avatar,omitempty"`
}
}
...
@@ -1751,6 +1752,7 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
...
@@ -1751,6 +1752,7 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
user
,
user
,
strings
.
TrimSpace
(
req
.
InvitationCode
),
strings
.
TrimSpace
(
req
.
InvitationCode
),
strings
.
TrimSpace
(
session
.
ProviderType
),
strings
.
TrimSpace
(
session
.
ProviderType
),
strings
.
TrimSpace
(
req
.
AffCode
),
);
err
!=
nil
{
);
err
!=
nil
{
_
=
tx
.
Rollback
()
_
=
tx
.
Rollback
()
if
rollbackCreatedUser
(
err
)
{
if
rollbackCreatedUser
(
err
)
{
...
...
backend/internal/handler/auth_oauth_pending_flow_test.go
View file @
3b7a5fff
...
@@ -2210,6 +2210,7 @@ CREATE TABLE IF NOT EXISTS user_avatars (
...
@@ -2210,6 +2210,7 @@ CREATE TABLE IF NOT EXISTS user_avatars (
nil
,
nil
,
nil
,
nil
,
options
.
defaultSubAssigner
,
options
.
defaultSubAssigner
,
nil
,
)
)
userSvc
:=
service
.
NewUserService
(
userRepo
,
nil
,
nil
,
nil
)
userSvc
:=
service
.
NewUserService
(
userRepo
,
nil
,
nil
,
nil
)
var
totpSvc
*
service
.
TotpService
var
totpSvc
*
service
.
TotpService
...
...
backend/internal/handler/auth_oidc_oauth.go
View file @
3b7a5fff
...
@@ -582,6 +582,7 @@ func (h *AuthHandler) createOIDCOAuthChoicePendingSession(
...
@@ -582,6 +582,7 @@ func (h *AuthHandler) createOIDCOAuthChoicePendingSession(
type
completeOIDCOAuthRequest
struct
{
type
completeOIDCOAuthRequest
struct
{
InvitationCode
string
`json:"invitation_code" binding:"required"`
InvitationCode
string
`json:"invitation_code" binding:"required"`
AffCode
string
`json:"aff_code,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptAvatar
*
bool
`json:"adopt_avatar,omitempty"`
AdoptAvatar
*
bool
`json:"adopt_avatar,omitempty"`
}
}
...
@@ -665,7 +666,7 @@ func (h *AuthHandler) CompleteOIDCOAuthRegistration(c *gin.Context) {
...
@@ -665,7 +666,7 @@ func (h *AuthHandler) CompleteOIDCOAuthRegistration(c *gin.Context) {
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
}
}
tokenPair
,
user
,
err
:=
h
.
authService
.
LoginOrRegisterOAuthWithTokenPair
(
c
.
Request
.
Context
(),
email
,
username
,
req
.
InvitationCode
)
tokenPair
,
user
,
err
:=
h
.
authService
.
LoginOrRegisterOAuthWithTokenPair
(
c
.
Request
.
Context
(),
email
,
username
,
req
.
InvitationCode
,
req
.
AffCode
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
...
...
backend/internal/handler/auth_session_revocation_test.go
View file @
3b7a5fff
...
@@ -35,7 +35,7 @@ func TestAuthHandlerRevokeAllSessionsInvalidatesAccessTokens(t *testing.T) {
...
@@ -35,7 +35,7 @@ func TestAuthHandlerRevokeAllSessionsInvalidatesAccessTokens(t *testing.T) {
ExpireHour
:
1
,
ExpireHour
:
1
,
},
},
}
}
authService
:=
service
.
NewAuthService
(
nil
,
repo
,
nil
,
refreshTokenCache
,
cfg
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
authService
:=
service
.
NewAuthService
(
nil
,
repo
,
nil
,
refreshTokenCache
,
cfg
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
)
handler
:=
&
AuthHandler
{
authService
:
authService
}
handler
:=
&
AuthHandler
{
authService
:
authService
}
recorder
:=
httptest
.
NewRecorder
()
recorder
:=
httptest
.
NewRecorder
()
...
...
backend/internal/handler/auth_wechat_oauth.go
View file @
3b7a5fff
...
@@ -481,6 +481,7 @@ func (h *AuthHandler) wechatPaymentResumeService() *service.PaymentResumeService
...
@@ -481,6 +481,7 @@ func (h *AuthHandler) wechatPaymentResumeService() *service.PaymentResumeService
type
completeWeChatOAuthRequest
struct
{
type
completeWeChatOAuthRequest
struct
{
InvitationCode
string
`json:"invitation_code" binding:"required"`
InvitationCode
string
`json:"invitation_code" binding:"required"`
AffCode
string
`json:"aff_code,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptDisplayName
*
bool
`json:"adopt_display_name,omitempty"`
AdoptAvatar
*
bool
`json:"adopt_avatar,omitempty"`
AdoptAvatar
*
bool
`json:"adopt_avatar,omitempty"`
}
}
...
@@ -547,7 +548,7 @@ func (h *AuthHandler) CompleteWeChatOAuthRegistration(c *gin.Context) {
...
@@ -547,7 +548,7 @@ func (h *AuthHandler) CompleteWeChatOAuthRegistration(c *gin.Context) {
return
return
}
}
tokenPair
,
user
,
err
:=
h
.
authService
.
LoginOrRegisterOAuthWithTokenPair
(
c
.
Request
.
Context
(),
email
,
username
,
req
.
InvitationCode
)
tokenPair
,
user
,
err
:=
h
.
authService
.
LoginOrRegisterOAuthWithTokenPair
(
c
.
Request
.
Context
(),
email
,
username
,
req
.
InvitationCode
,
req
.
AffCode
)
if
err
!=
nil
{
if
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
...
...
backend/internal/handler/auth_wechat_oauth_test.go
View file @
3b7a5fff
...
@@ -1399,6 +1399,7 @@ func newWeChatOAuthTestHandlerWithSettings(t *testing.T, invitationEnabled bool,
...
@@ -1399,6 +1399,7 @@ func newWeChatOAuthTestHandlerWithSettings(t *testing.T, invitationEnabled bool,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
nil
,
)
)
return
&
AuthHandler
{
return
&
AuthHandler
{
...
...
backend/internal/handler/dto/settings.go
View file @
3b7a5fff
...
@@ -106,10 +106,14 @@ type SystemSettings struct {
...
@@ -106,10 +106,14 @@ type SystemSettings struct {
CustomMenuItems
[]
CustomMenuItem
`json:"custom_menu_items"`
CustomMenuItems
[]
CustomMenuItem
`json:"custom_menu_items"`
CustomEndpoints
[]
CustomEndpoint
`json:"custom_endpoints"`
CustomEndpoints
[]
CustomEndpoint
`json:"custom_endpoints"`
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultConcurrency
int
`json:"default_concurrency"`
DefaultBalance
float64
`json:"default_balance"`
DefaultBalance
float64
`json:"default_balance"`
DefaultUserRPMLimit
int
`json:"default_user_rpm_limit"`
AffiliateRebateRate
float64
`json:"affiliate_rebate_rate"`
DefaultSubscriptions
[]
DefaultSubscriptionSetting
`json:"default_subscriptions"`
AffiliateRebateFreezeHours
int
`json:"affiliate_rebate_freeze_hours"`
AffiliateRebateDurationDays
int
`json:"affiliate_rebate_duration_days"`
AffiliateRebatePerInviteeCap
float64
`json:"affiliate_rebate_per_invitee_cap"`
DefaultUserRPMLimit
int
`json:"default_user_rpm_limit"`
DefaultSubscriptions
[]
DefaultSubscriptionSetting
`json:"default_subscriptions"`
// Model fallback configuration
// Model fallback configuration
EnableModelFallback
bool
`json:"enable_model_fallback"`
EnableModelFallback
bool
`json:"enable_model_fallback"`
...
@@ -191,6 +195,9 @@ type SystemSettings struct {
...
@@ -191,6 +195,9 @@ type SystemSettings struct {
// Available Channels feature switch (user-facing aggregate view)
// Available Channels feature switch (user-facing aggregate view)
AvailableChannelsEnabled
bool
`json:"available_channels_enabled"`
AvailableChannelsEnabled
bool
`json:"available_channels_enabled"`
// Affiliate (邀请返利) feature switch
AffiliateEnabled
bool
`json:"affiliate_enabled"`
}
}
type
DefaultSubscriptionSetting
struct
{
type
DefaultSubscriptionSetting
struct
{
...
@@ -243,6 +250,8 @@ type PublicSettings struct {
...
@@ -243,6 +250,8 @@ type PublicSettings struct {
ChannelMonitorDefaultIntervalSeconds
int
`json:"channel_monitor_default_interval_seconds"`
ChannelMonitorDefaultIntervalSeconds
int
`json:"channel_monitor_default_interval_seconds"`
AvailableChannelsEnabled
bool
`json:"available_channels_enabled"`
AvailableChannelsEnabled
bool
`json:"available_channels_enabled"`
AffiliateEnabled
bool
`json:"affiliate_enabled"`
}
}
// OverloadCooldownSettings 529过载冷却配置 DTO
// OverloadCooldownSettings 529过载冷却配置 DTO
...
...
backend/internal/handler/gateway_handler.go
View file @
3b7a5fff
...
@@ -175,6 +175,15 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
...
@@ -175,6 +175,15 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
reqStream
:=
parsedReq
.
Stream
reqStream
:=
parsedReq
.
Stream
reqLog
=
reqLog
.
With
(
zap
.
String
(
"model"
,
reqModel
),
zap
.
Bool
(
"stream"
,
reqStream
))
reqLog
=
reqLog
.
With
(
zap
.
String
(
"model"
,
reqModel
),
zap
.
Bool
(
"stream"
,
reqStream
))
// 若 request_capture 已启用且为流式请求,注入响应体采集 buffer 到 context
// service 层的 handleStreamingResponse 会将 text_delta 内容写入此 buffer
if
captureID
>
0
&&
reqStream
{
captureRespBuilder
:=
&
strings
.
Builder
{}
c
.
Request
=
c
.
Request
.
WithContext
(
context
.
WithValue
(
c
.
Request
.
Context
(),
ctxkey
.
ResponseCaptureBuffer
,
captureRespBuilder
),
)
}
// 解析渠道级模型映射
// 解析渠道级模型映射
channelMapping
,
_
:=
h
.
gatewayService
.
ResolveChannelMappingAndRestrict
(
c
.
Request
.
Context
(),
apiKey
.
GroupID
,
reqModel
)
channelMapping
,
_
:=
h
.
gatewayService
.
ResolveChannelMappingAndRestrict
(
c
.
Request
.
Context
(),
apiKey
.
GroupID
,
reqModel
)
...
@@ -500,6 +509,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
...
@@ -500,6 +509,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
result
.
ReasoningEffort
=
service
.
NormalizeClaudeOutputEffort
(
parsedReq
.
OutputEffort
)
result
.
ReasoningEffort
=
service
.
NormalizeClaudeOutputEffort
(
parsedReq
.
OutputEffort
)
}
}
// 异步写入响应体到捕获记录
if
captureID
>
0
&&
h
.
requestCaptureService
!=
nil
{
h
.
requestCaptureService
.
CaptureResponse
(
captureID
,
result
.
ResponseBody
)
}
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
// 使用量记录通过有界 worker 池提交,避免请求热路径创建无界 goroutine。
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
...
...
backend/internal/handler/gateway_handler_chat_completions.go
View file @
3b7a5fff
...
@@ -62,9 +62,10 @@ func (h *GatewayHandler) ChatCompletions(c *gin.Context) {
...
@@ -62,9 +62,10 @@ func (h *GatewayHandler) ChatCompletions(c *gin.Context) {
}
}
// 异步捕获请求体(仅当该 API Key 开启了 capture_requests)
// 异步捕获请求体(仅当该 API Key 开启了 capture_requests)
var
captureID
int64
if
apiKey
.
CaptureRequests
&&
h
.
requestCaptureService
!=
nil
{
if
apiKey
.
CaptureRequests
&&
h
.
requestCaptureService
!=
nil
{
requestID
,
_
:=
c
.
Request
.
Context
()
.
Value
(
ctxkey
.
RequestID
)
.
(
string
)
requestID
,
_
:=
c
.
Request
.
Context
()
.
Value
(
ctxkey
.
RequestID
)
.
(
string
)
h
.
requestCaptureService
.
Capture
(
captureID
=
h
.
requestCaptureService
.
Capture
(
apiKey
.
ID
,
subject
.
UserID
,
apiKey
.
ID
,
subject
.
UserID
,
requestID
,
requestID
,
c
.
Request
.
URL
.
Path
,
c
.
Request
.
URL
.
Path
,
...
@@ -267,6 +268,11 @@ func (h *GatewayHandler) ChatCompletions(c *gin.Context) {
...
@@ -267,6 +268,11 @@ func (h *GatewayHandler) ChatCompletions(c *gin.Context) {
inboundEndpoint
:=
GetInboundEndpoint
(
c
)
inboundEndpoint
:=
GetInboundEndpoint
(
c
)
upstreamEndpoint
:=
GetUpstreamEndpoint
(
c
,
account
.
Platform
)
upstreamEndpoint
:=
GetUpstreamEndpoint
(
c
,
account
.
Platform
)
// 异步写入响应体到捕获记录
if
captureID
>
0
&&
h
.
requestCaptureService
!=
nil
{
h
.
requestCaptureService
.
CaptureResponse
(
captureID
,
result
.
ResponseBody
)
}
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
h
.
submitUsageRecordTask
(
func
(
ctx
context
.
Context
)
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
if
err
:=
h
.
gatewayService
.
RecordUsage
(
ctx
,
&
service
.
RecordUsageInput
{
Result
:
result
,
Result
:
result
,
...
...
backend/internal/handler/handler.go
View file @
3b7a5fff
...
@@ -34,6 +34,7 @@ type AdminHandlers struct {
...
@@ -34,6 +34,7 @@ type AdminHandlers struct {
ChannelMonitor
*
admin
.
ChannelMonitorHandler
ChannelMonitor
*
admin
.
ChannelMonitorHandler
ChannelMonitorTemplate
*
admin
.
ChannelMonitorRequestTemplateHandler
ChannelMonitorTemplate
*
admin
.
ChannelMonitorRequestTemplateHandler
Payment
*
admin
.
PaymentHandler
Payment
*
admin
.
PaymentHandler
Affiliate
*
admin
.
AffiliateHandler
}
}
// Handlers contains all HTTP handlers
// Handlers contains all HTTP handlers
...
...
Prev
1
2
3
4
5
…
9
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