Initial sanitized code sync
This commit is contained in:
125
internal/service/cpa_svc.go
Normal file
125
internal/service/cpa_svc.go
Normal file
@@ -0,0 +1,125 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user