package sub import ( "encoding/json" "fmt" "net/http" "strconv" "strings" ) type reportSummaryResp struct { Success bool `json:"success"` Data struct { Total int `json:"total"` Currency []struct { Currency string `json:"currency"` Cnt int `json:"cnt"` Total float64 `json:"total"` } `json:"currency"` Due struct { Overdue int `json:"overdue"` Due7 int `json:"due7"` Due30 int `json:"due30"` } `json:"due"` } `json:"data"` Error string `json:"error"` } type reportListResp struct { Success bool `json:"success"` Data []map[string]any `json:"data"` Error string `json:"error"` } func (m *Module) report(userID int64, args []string) (string, error) { cfg, err := m.loadSettings() if err != nil { return "❌ " + err.Error(), nil } typeName := "summary" days := "" if len(args) >= 1 { typeName = strings.ToLower(strings.TrimSpace(args[0])) } if len(args) >= 2 { days = strings.TrimSpace(args[1]) } base := strings.TrimRight(cfg.APIBase, "/") + "/api/report?user_id=1&channel=qq&type=" + typeName if days != "" { base += "&days=" + days } body, status, err := m.doRequest(http.MethodGet, base, nil, cfg) if err != nil { return "❌ 报表请求失败: " + err.Error(), nil } if status >= 400 { return fmt.Sprintf("❌ 报表失败: http %d %s", status, body), nil } switch typeName { case "summary": var out reportSummaryResp if err := json.Unmarshal([]byte(body), &out); err != nil { return "❌ 报表解析失败", nil } if !out.Success { return "❌ " + out.Error, nil } lines := []string{fmt.Sprintf("📊 订阅总览:%d 项", out.Data.Total)} if len(out.Data.Currency) > 0 { lines = append(lines, "💰 按币种汇总:") for _, c := range out.Data.Currency { lines = append(lines, fmt.Sprintf("- %s: %.2f(%d项)", c.Currency, c.Total, c.Cnt)) } } lines = append(lines, fmt.Sprintf("⏰ 到期:7天内=%d,30天内=%d,已逾期=%d", out.Data.Due.Due7, out.Data.Due.Due30, out.Data.Due.Overdue)) return strings.Join(lines, "\n"), nil case "category", "cycle", "due": var out reportListResp if err := json.Unmarshal([]byte(body), &out); err != nil { return "❌ 报表解析失败", nil } if !out.Success { return "❌ " + out.Error, nil } if len(out.Data) == 0 { return "📭 无数据", nil } lines := []string{fmt.Sprintf("📊 报表(%s):%d 条", typeName, len(out.Data))} max := len(out.Data) if max > 30 { max = 30 } for i := 0; i < max; i++ { r := out.Data[i] switch typeName { case "category": lines = append(lines, fmt.Sprintf("- %v | %v | 数量:%v | 金额:%.2f", r["category"], r["currency"], r["cnt"], toF64(r["total"]))) case "cycle": lines = append(lines, fmt.Sprintf("- %v x%v | 数量:%v", r["cycle_unit"], r["cycle_value"], r["cnt"])) case "due": lines = append(lines, fmt.Sprintf("- %v | %v | 分类:%v | 到期:%v | 周期:%v x%v | %.2f %v", r["id"], r["name"], r["category"], r["due_date"], r["cycle_unit"], r["cycle_value"], toF64(r["amount"]), r["currency"])) } } if len(out.Data) > max { lines = append(lines, fmt.Sprintf("... 共%d条,仅显示前%d条", len(out.Data), max)) } return strings.Join(lines, "\n"), nil default: return "❌ 用法:/sub report | /sub report category | /sub report cycle | /sub report due <7|30|overdue>", nil } } func toF64(v any) float64 { switch t := v.(type) { case float64: return t case string: f, _ := strconv.ParseFloat(t, 64) return f case int: return float64(t) default: return 0 } }