Files
gpt-plus-gpt/internal/service/cpa_svc.go
2026-03-15 20:48:19 +08:00

126 lines
3.4 KiB
Go

package service
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"gpt-plus/internal/db"
"gorm.io/gorm"
)
// getConfigValue reads a config value from the database, auto-decrypting password fields.
func getConfigValue(d *gorm.DB, key string) (string, error) {
var cfg db.SystemConfig
if err := d.Where("key = ?", key).First(&cfg).Error; err != nil {
return "", err
}
if cfg.Type == "password" && cfg.Value != "" {
decrypted, err := db.Decrypt(cfg.Value)
if err != nil {
// Fallback to raw value (may be plaintext from seed)
return cfg.Value, nil
}
return decrypted, nil
}
return cfg.Value, nil
}
// TransferResult holds the result for a single account transfer.
type TransferResult struct {
ID uint `json:"id"`
Email string `json:"email"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
}
// TransferAccountToCPA builds an auth file for the account and uploads it to CPA.
func TransferAccountToCPA(d *gorm.DB, accountID uint) TransferResult {
// Get account first (for email in result)
var acct db.Account
if err := d.First(&acct, accountID).Error; err != nil {
return TransferResult{ID: accountID, Error: "账号不存在"}
}
result := TransferResult{ID: acct.ID, Email: acct.Email}
// Get CPA config
baseURL, err := getConfigValue(d, "cpa.base_url")
if err != nil || baseURL == "" {
result.Error = "CPA 地址未配置"
return result
}
managementKey, err := getConfigValue(d, "cpa.management_key")
if err != nil || managementKey == "" {
result.Error = "CPA Management Key 未配置"
return result
}
// Build auth file
auth := buildAuthFile(&acct)
jsonData, err := json.MarshalIndent(auth, "", " ")
if err != nil {
result.Error = fmt.Sprintf("序列化失败: %v", err)
return result
}
// If team_owner, also transfer sub-accounts
if acct.Plan == "team_owner" {
var subs []db.Account
d.Where("parent_id = ?", acct.ID).Find(&subs)
for _, sub := range subs {
subAuth := buildAuthFile(&sub)
subData, _ := json.MarshalIndent(subAuth, "", " ")
if err := uploadAuthFile(baseURL, managementKey, sub.Email+".auth.json", subData); err != nil {
result.Error = fmt.Sprintf("子号 %s 上传失败: %v", sub.Email, err)
return result
}
}
}
// Upload main account
if err := uploadAuthFile(baseURL, managementKey, acct.Email+".auth.json", jsonData); err != nil {
result.Error = fmt.Sprintf("上传失败: %v", err)
return result
}
result.OK = true
return result
}
// uploadAuthFile uploads a single auth JSON file to CPA via multipart form.
func uploadAuthFile(baseURL, managementKey, filename string, data []byte) error {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", filename)
if err != nil {
return fmt.Errorf("创建表单失败: %w", err)
}
part.Write(data)
writer.Close()
req, err := http.NewRequest("POST", baseURL+"/v0/management/auth-files", &body)
if err != nil {
return fmt.Errorf("创建请求失败: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+managementKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("CPA 返回 %d: %s", resp.StatusCode, string(respBody))
}
return nil
}