package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "time" ) // 运行方式 // export FWH_TOKEN="你的logintoken值" // cd service/test/tou // go run main.go // ===== 配置 ===== const ( baseURL = "https://test.franklinwh.com" lang = "ZH_CN" ) // ===== 数据结构 ===== type APIResponse struct { Code int `json:"code"` Message string `json:"message"` Result json.RawMessage `json:"result"` Success bool `json:"success"` } type Province struct { ID int `json:"id"` NameEn string `json:"nameEn"` NameZh string `json:"nameZh"` } type TemplateData struct { Template TemplateInfo `json:"template"` StrategyList []StrategyInfo `json:"strategyList"` } type TemplateInfo struct { ID int `json:"id"` ElectricCompany string `json:"electricCompany"` EleCompanyFullName string `json:"eleCompanyFullName"` DerSchdule string `json:"derSchdule"` GridType string `json:"gridType"` Name string `json:"name"` ElectricityType int `json:"electricityType"` CountryId int `json:"countryId"` ProvinceId int `json:"provinceId"` ProvinceEn string `json:"provinceEn"` Env int `json:"env"` MookRunCount int `json:"mookRunCount"` Priority int `json:"priority"` } type StrategyInfo struct { SeasonName string `json:"seasonName"` Month string `json:"month"` DayName string `json:"dayName"` DayType int `json:"dayType"` CcociateSn string `json:"ccociateSn"` CcociateType int `json:"ccociateType"` EleticRatePeak *float64 `json:"eleticRatePeak"` EleticRateShoulder *float64 `json:"eleticRateShoulder"` EleticRateValley *float64 `json:"eleticRateValley"` EleticRateSharp *float64 `json:"eleticRateSharp"` EleticRateSuperOffPeak *float64 `json:"eleticRateSuperOffPeak"` EleticRateGridFee *float64 `json:"eleticRateGridFee"` EleticSellPeak *float64 `json:"eleticSellPeak"` EleticSellShoulder *float64 `json:"eleticSellShoulder"` EleticSellValley *float64 `json:"eleticSellValley"` EleticSellSharp *float64 `json:"eleticSellSharp"` EleticSellSuperOffPeak *float64 `json:"eleticSellSuperOffPeak"` DetailVoList []DetailInfo `json:"detailVoList"` } type DetailInfo struct { StartHourTime string `json:"startHourTime"` EndHourTime string `json:"endHourTime"` WaveType int `json:"waveType"` Name string `json:"name"` BriefDescribe string `json:"briefDescribe"` SolarPriority string `json:"solarPriority"` GridDischargeMax *float64 `json:"gridDischargeMax"` GridChargeMax *float64 `json:"gridChargeMax"` ChargePower *float64 `json:"chargePower"` DischargePower *float64 `json:"dischargePower"` ChargeMax *float64 `json:"chargeMax"` DischargeMax *float64 `json:"dischargeMax"` GcaoMax *float64 `json:"gcaoMax"` LoadPriority string `json:"loadPriority"` GridFeedMax *float64 `json:"gridFeedMax"` GridMax *float64 `json:"gridMax"` MinDischargeSoc *int `json:"minDischargeSoc"` MaxChargeSoc *int `json:"maxChargeSoc"` HeatEnable *int `json:"heatEnable"` PowerOffApower string `json:"powerOffApower"` RampTime *int `json:"rampTime"` OffGrid *int `json:"offGrid"` SolarCutoff *int `json:"solarCutoff"` UseModeFlag int `json:"useModeFlag"` DispatchId int `json:"dispatchId"` BuyRate *float64 `json:"buyRate"` SellRate *float64 `json:"sellRate"` } // 创建模版的请求体 type CreateTemplatePayload struct { Template CreateTemplateInfo `json:"template"` StrategyList []StrategyInfo `json:"strategyList"` Lang string `json:"lang"` } type CreateTemplateInfo struct { CountryId int `json:"countryId"` ProvinceId int `json:"provinceId"` DerSchdule string `json:"derSchdule"` EleCompanyFullName string `json:"eleCompanyFullName"` ElectricCompany string `json:"electricCompany"` GridType string `json:"gridType"` ElectricityType int `json:"electricityType"` Name string `json:"name"` MookRunCount string `json:"mookRunCount"` Priority string `json:"priority"` Env string `json:"env"` } // ===== HAR 数据:三个模版的策略数据 ===== func getTemplateData() []TemplateData { // 模版 4477 - ZEROHERO (GloBird Energy, Queensland) int90 := 90 float5 := 5.0 float0_1 := 0.1 float10 := 10.0 float0 := 0.0 int1 := 1 int0 := 0 template4477 := TemplateData{ Template: TemplateInfo{ ID: 4477, ElectricCompany: "GloBird Energy", EleCompanyFullName: "GloBird Energy", DerSchdule: "ZEROHERO", GridType: "ZEROHERO", Name: "ZEROHERO", ElectricityType: 1, CountryId: 3, ProvinceId: 88, ProvinceEn: "Queensland", Env: 0, MookRunCount: 0, Priority: 0, }, StrategyList: []StrategyInfo{ { SeasonName: "All months", Month: "1,2,3,4,5,6,7,8,9,10,11,12", DayName: "Every day", DayType: 3, CcociateSn: "", CcociateType: 1, DetailVoList: []DetailInfo{ {StartHourTime: "00:00", EndHourTime: "11:00", WaveType: 1, Name: "mid-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, {StartHourTime: "11:00", EndHourTime: "14:00", WaveType: 0, Name: "off-peak", BriefDescribe: "aPower charger from solar", SolarPriority: "1,2,3", GridChargeMax: &float5, LoadPriority: "1,2,0", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 3, PowerOffApower: ""}, {StartHourTime: "14:00", EndHourTime: "16:00", WaveType: 1, Name: "mid-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", MinDischargeSoc: &int90, SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, {StartHourTime: "16:00", EndHourTime: "18:00", WaveType: 2, Name: "on-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", MinDischargeSoc: &int90, SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, {StartHourTime: "18:00", EndHourTime: "21:00", WaveType: 2, Name: "on-peak", BriefDescribe: "Zero Import", SolarPriority: "9,9,9", GridDischargeMax: &float0_1, GridFeedMax: &float0_1, DischargePower: &float10, GridMax: &float0, GcaoMax: &float0, LoadPriority: "9,9,9", HeatEnable: &int1, SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 9, PowerOffApower: ""}, {StartHourTime: "21:00", EndHourTime: "23:00", WaveType: 2, Name: "on-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, {StartHourTime: "23:00", EndHourTime: "24:00", WaveType: 1, Name: "mid-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, }, }, }, } // 模版 4478 - FOUR4FREE (GloBird Energy, Queensland) template4478 := TemplateData{ Template: TemplateInfo{ ID: 4478, ElectricCompany: "GloBird Energy", EleCompanyFullName: "GloBird Energy", DerSchdule: "FOUR4FREE", GridType: "FOUR4FREE", Name: "FOUR4FREE", ElectricityType: 1, CountryId: 3, ProvinceId: 88, ProvinceEn: "Queensland", Env: 0, MookRunCount: 0, Priority: 0, }, StrategyList: []StrategyInfo{ { SeasonName: "All months", Month: "1,2,3,4,5,6,7,8,9,10,11,12", DayName: "Every day", DayType: 3, CcociateSn: "", CcociateType: 1, DetailVoList: []DetailInfo{ {StartHourTime: "00:00", EndHourTime: "10:00", WaveType: 1, Name: "mid-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, {StartHourTime: "10:00", EndHourTime: "14:00", WaveType: 0, Name: "off-peak", BriefDescribe: "aPower charge from solar", SolarPriority: "1,2,3", GridChargeMax: &float5, LoadPriority: "1,2,0", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 3, PowerOffApower: ""}, {StartHourTime: "14:00", EndHourTime: "16:00", WaveType: 1, Name: "mid-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", MinDischargeSoc: &int90, SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, {StartHourTime: "16:00", EndHourTime: "23:00", WaveType: 2, Name: "on-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, {StartHourTime: "23:00", EndHourTime: "24:00", WaveType: 1, Name: "mid-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, }, }, }, } // 模版 4479 - Basic 3 Free (OVO Energy, Queensland) template4479 := TemplateData{ Template: TemplateInfo{ ID: 4479, ElectricCompany: "OVO Energy", EleCompanyFullName: "OVO Energy", DerSchdule: "Basic 3 Free", GridType: "Basic 3 Free", Name: "Basic 3 Free", ElectricityType: 1, CountryId: 3, ProvinceId: 88, ProvinceEn: "Queensland", Env: 0, MookRunCount: 0, Priority: 0, }, StrategyList: []StrategyInfo{ { SeasonName: "All months", Month: "1,2,3,4,5,6,7,8,9,10,11,12", DayName: "Every day", DayType: 3, CcociateSn: "", CcociateType: 1, DetailVoList: []DetailInfo{ {StartHourTime: "00:00", EndHourTime: "11:00", WaveType: 1, Name: "mid-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, {StartHourTime: "11:00", EndHourTime: "14:00", WaveType: 0, Name: "off-peak", BriefDescribe: "aPower charge from solar", SolarPriority: "1,2,3", GridChargeMax: &float5, LoadPriority: "1,2,0", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 3, PowerOffApower: ""}, {StartHourTime: "14:00", EndHourTime: "16:00", WaveType: 1, Name: "mid-peak", BriefDescribe: "Self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", MinDischargeSoc: &int90, SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, {StartHourTime: "16:00", EndHourTime: "23:00", WaveType: 2, Name: "on-peak", BriefDescribe: "self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, {StartHourTime: "23:00", EndHourTime: "24:00", WaveType: 1, Name: "mid-peak", BriefDescribe: "self-consumption", SolarPriority: "1,3,2", LoadPriority: "1,3,2", SolarCutoff: &int0, UseModeFlag: 0, DispatchId: 6, PowerOffApower: ""}, }, }, }, } return []TemplateData{template4477, template4478, template4479} } // ===== HAR 数据:澳洲所有州 ===== func getAustraliaProvinces() []Province { return []Province{ {ID: 93, NameEn: "Australian Capital Territory", NameZh: "澳大利亚首都特区"}, {ID: 95, NameEn: "Jervis Bay Territory", NameZh: "杰维斯湾领地"}, {ID: 87, NameEn: "New South Wales", NameZh: "新南威尔士州"}, {ID: 94, NameEn: "Northern Territory", NameZh: "北领地"}, {ID: 88, NameEn: "Queensland", NameZh: "昆士兰州"}, {ID: 89, NameEn: "South Australia", NameZh: "南澳大利亚州"}, {ID: 90, NameEn: "Tasmania", NameZh: "塔斯马尼亚州"}, {ID: 91, NameEn: "Victoria", NameZh: "維多利亞省"}, {ID: 92, NameEn: "Western Australia", NameZh: "西澳大利亚州"}, } } // ===== HTTP 客户端 ===== type Client struct { httpClient *http.Client token string } func NewClient(token string) *Client { return &Client{ httpClient: &http.Client{Timeout: 30 * time.Second}, token: token, } } // POST JSON 请求 func (c *Client) PostJSON(path string, payload interface{}) ([]byte, error) { reqURL := baseURL + path jsonData, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("序列化请求体失败: %w", err) } req, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Logintoken", c.token) req.Header.Set("Lang", lang) req.Header.Set("Accept", "application/json, text/plain, */*") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP状态码: %d, 响应: %s", resp.StatusCode, string(body)) } return body, nil } // ===== 业务逻辑 ===== // 创建模版 func createTemplate(client *Client, payload *CreateTemplatePayload) (int, error) { body, err := client.PostJSON("/hes-gateway/manage/updateOrSaveIotTouTemplate", payload) if err != nil { return 0, err } var resp APIResponse if err := json.Unmarshal(body, &resp); err != nil { return 0, fmt.Errorf("解析创建模版响应失败: %w", err) } if resp.Code != 200 { return 0, fmt.Errorf("创建模版失败: %s", resp.Message) } var newID int if err := json.Unmarshal(resp.Result, &newID); err != nil { return 0, fmt.Errorf("解析新模版ID失败: %w", err) } return newID, nil } // 构建创建模版的请求体 func buildCreatePayload(tmplData *TemplateData, province Province) *CreateTemplatePayload { return &CreateTemplatePayload{ Template: CreateTemplateInfo{ CountryId: tmplData.Template.CountryId, ProvinceId: province.ID, DerSchdule: tmplData.Template.DerSchdule, EleCompanyFullName: tmplData.Template.EleCompanyFullName, ElectricCompany: tmplData.Template.ElectricCompany, GridType: tmplData.Template.GridType, ElectricityType: tmplData.Template.ElectricityType, Name: tmplData.Template.Name, MookRunCount: fmt.Sprintf("%d", tmplData.Template.MookRunCount), Priority: fmt.Sprintf("%d", tmplData.Template.Priority), Env: fmt.Sprintf("%d", tmplData.Template.Env), }, StrategyList: tmplData.StrategyList, Lang: lang, } } func main() { // 从环境变量获取 token token := os.Getenv("FWH_TOKEN") if token == "" { fmt.Println("请设置 FWH_TOKEN 环境变量") fmt.Println("例如: export FWH_TOKEN=eyJhbGciOiJIUzI1NiIs...") os.Exit(1) } client := NewClient(token) // 获取硬编码的模版数据和州列表 templates := getTemplateData() allProvinces := getAustraliaProvinces() // 收集已有模版对应的州ID(Queensland, id=88) existingProvinceIDs := make(map[int]bool) for _, tmpl := range templates { existingProvinceIDs[tmpl.Template.ProvinceId] = true } // 过滤出需要创建的州 var targetProvinces []Province for _, p := range allProvinces { if !existingProvinceIDs[p.ID] { targetProvinces = append(targetProvinces, p) } } fmt.Printf("模版数量: %d, 目标州数量: %d (已排除: Queensland)\n", len(templates), len(targetProvinces)) fmt.Printf("预计创建模版总数: %d\n\n", len(templates)*len(targetProvinces)) // 为每个目标州创建所有模版 successCount := 0 failCount := 0 for _, province := range targetProvinces { fmt.Printf("--- 州: %s (%s) ---\n", province.NameEn, province.NameZh) for _, tmplData := range templates { payload := buildCreatePayload(&tmplData, province) newID, err := createTemplate(client, payload) if err != nil { fmt.Printf(" ❌ [%s] 创建失败: %v\n", tmplData.Template.Name, err) failCount++ continue } fmt.Printf(" ✅ [%s] 创建成功, 新ID: %d\n", tmplData.Template.Name, newID) successCount++ // 请求间隔,避免过快 time.Sleep(500 * time.Millisecond) } fmt.Println() } // 输出统计 fmt.Printf("===== 完成 =====\n") fmt.Printf("成功: %d, 失败: %d, 总计: %d\n", successCount, failCount, successCount+failCount) }