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 }