package services import ( "fmt" "net/url" "strings" "time" "fwh-toolbox-server/domain" ) // Each document kind owns its content identity version. Bump only the affected // kind when its query, parsing, or stored content semantics change. var dataDownloadContentIdentityVersion = map[domain.DataDownloadKind]string{ domain.DataDownloadPowerFlow: "v1", domain.DataDownloadMqttLog: "v1", domain.DataDownloadEventAlarm: "v1", domain.DataDownloadAlarmSnapshot: "v1", domain.DataDownloadEmsLog: "v1", domain.DataDownloadFaultRecord: "v2", domain.DataDownloadUpdateLog: "v1", } type dataDownloadRequestKeyField struct { name string value string } // buildDataDownloadRequestKey identifies the requested logical document. The // fields intentionally differ by kind so task-only or irrelevant parameters do // not reduce reuse between otherwise equivalent requests. func buildDataDownloadRequestKey(item *domain.DataDownloadItem) (string, error) { if item == nil { return "", fmt.Errorf("data download item is nil") } contentVersion, ok := dataDownloadContentIdentityVersion[item.Kind] if !ok { return "", fmt.Errorf("unsupported data download kind %q", item.Kind) } if item.SiteSN == "" { return "", fmt.Errorf("site SN is required for %s", item.Kind) } fields := []dataDownloadRequestKeyField{ {name: "kind", value: string(item.Kind)}, {name: "content_version", value: contentVersion}, } addSite := func() { fields = append(fields, dataDownloadRequestKeyField{name: "site_sn", value: item.SiteSN}) } addDate := func() error { if _, err := time.Parse(time.DateOnly, item.Date); err != nil { return fmt.Errorf("invalid data date %q for %s", item.Date, item.Kind) } fields = append(fields, dataDownloadRequestKeyField{name: "date", value: item.Date}) return nil } addSubtype := func() error { if item.Subtype == "" { return fmt.Errorf("subtype is required for %s", item.Kind) } fields = append(fields, dataDownloadRequestKeyField{name: "subtype", value: item.Subtype}) return nil } addTimezone := func() error { if item.Timezone == "" { return fmt.Errorf("timezone is required for %s", item.Kind) } fields = append(fields, dataDownloadRequestKeyField{name: "timezone", value: item.Timezone}) return nil } switch item.Kind { case domain.DataDownloadPowerFlow, domain.DataDownloadEventAlarm, domain.DataDownloadAlarmSnapshot: addSite() if err := addDate(); err != nil { return "", err } if err := addTimezone(); err != nil { return "", err } case domain.DataDownloadMqttLog: addSite() if err := addDate(); err != nil { return "", err } if err := addSubtype(); err != nil { return "", err } if err := addTimezone(); err != nil { return "", err } case domain.DataDownloadEmsLog: addSite() if err := addDate(); err != nil { return "", err } if err := addSubtype(); err != nil { return "", err } case domain.DataDownloadFaultRecord: if item.DeviceSN == "" || item.DeviceType == "" { return "", fmt.Errorf("device SN and device type are required for %s", item.Kind) } fields = append(fields, dataDownloadRequestKeyField{name: "device_sn", value: item.DeviceSN}, dataDownloadRequestKeyField{name: "device_type", value: item.DeviceType}, ) if err := addDate(); err != nil { return "", err } if err := addTimezone(); err != nil { return "", err } case domain.DataDownloadUpdateLog: addSite() } var key strings.Builder key.WriteString("dd-request-v1") for _, field := range fields { key.WriteByte('|') key.WriteString(field.name) key.WriteByte('=') key.WriteString(url.QueryEscape(field.value)) } return key.String(), nil }