126 lines
3.4 KiB
Go
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
|
|
}
|