//go:build llm_script package main import ( "database/sql" "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" "time" _ "github.com/lib/pq" ) type CoverageDay struct { Date string OfficialRelease bool PromoCampaign bool NewModel bool PriceChange bool } func (d CoverageDay) HasTrueEvent() bool { return d.OfficialRelease || d.PromoCampaign || d.NewModel || d.PriceChange } func (d CoverageDay) EventLabels() []string { var labels []string if d.OfficialRelease { labels = append(labels, "official_release") } if d.PromoCampaign { labels = append(labels, "promo_campaign") } if d.NewModel { labels = append(labels, "new_model") } if d.PriceChange { labels = append(labels, "price_change") } return labels } type CoverageSummary struct { TotalDays int CoveredDays int CoveragePct float64 } type CoveragePromoDefinition struct { Date string `json:"date"` } func main() { if len(os.Args) != 3 { fmt.Fprintln(os.Stderr, "用法: go run -tags llm_script scripts/report_event_coverage.go YYYY-MM-DD YYYY-MM-DD") os.Exit(2) } startDate, endDate, err := parseCoverageArgs(os.Args[1], os.Args[2]) if err != nil { fmt.Fprintln(os.Stderr, "参数错误:", err) os.Exit(2) } dsn := os.Getenv("DATABASE_URL") if dsn == "" { dsn = "postgres://long@/llm_intelligence?host=/var/run/postgresql" } db, err := sql.Open("postgres", dsn) if err != nil { fmt.Fprintln(os.Stderr, "连接数据库失败:", err) os.Exit(1) } defer db.Close() days, err := collectCoverageDays(db, startDate, endDate) if err != nil { fmt.Fprintln(os.Stderr, "统计覆盖率失败:", err) os.Exit(1) } if len(days) == 0 { fmt.Fprintln(os.Stderr, "指定范围内没有已生成的日报记录") os.Exit(1) } summary := summarizeCoverage(days) fmt.Printf("V2 事件覆盖率报告 [%s, %s]\n", startDate, endDate) for _, day := range days { status := "MISS" if day.HasTrueEvent() { status = "PASS" } fmt.Printf("%s\t%s\t%s\n", day.Date, status, strings.Join(day.EventLabels(), ", ")) } fmt.Printf("\n总日报天数: %d\n", summary.TotalDays) fmt.Printf("命中真实变化事件天数: %d\n", summary.CoveredDays) fmt.Printf("覆盖率: %.2f%%\n", summary.CoveragePct) if summary.CoveragePct < 80 { os.Exit(1) } } func parseCoverageArgs(start, end string) (string, string, error) { startDate, err := time.Parse("2006-01-02", start) if err != nil { return "", "", fmt.Errorf("开始日期格式不正确") } endDate, err := time.Parse("2006-01-02", end) if err != nil { return "", "", fmt.Errorf("结束日期格式不正确") } if endDate.Before(startDate) { return "", "", fmt.Errorf("结束日期不能早于开始日期") } return startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nil } func collectCoverageDays(db *sql.DB, startDate, endDate string) ([]CoverageDay, error) { reportDates, err := loadGeneratedReportDates(db, startDate, endDate) if err != nil { return nil, err } promoDates, err := loadCoveragePromoDates() if err != nil { return nil, err } var days []CoverageDay for _, date := range reportDates { officialRelease, err := hasOfficialRelease(db, date) if err != nil { return nil, err } newModel, err := hasNewModel(db, date) if err != nil { return nil, err } priceChange, err := hasPriceChange(db, date) if err != nil { return nil, err } days = append(days, CoverageDay{ Date: date, OfficialRelease: officialRelease, PromoCampaign: promoDates[date], NewModel: newModel, PriceChange: priceChange, }) } sort.Slice(days, func(i, j int) bool { return days[i].Date < days[j].Date }) return days, nil } func loadGeneratedReportDates(db *sql.DB, startDate, endDate string) ([]string, error) { rows, err := db.Query(` SELECT DISTINCT report_date::text FROM daily_report WHERE report_date BETWEEN $1::date AND $2::date AND status = 'generated' ORDER BY report_date `, startDate, endDate) if err != nil { return nil, err } defer rows.Close() var dates []string for rows.Next() { var date string if err := rows.Scan(&date); err != nil { return nil, err } dates = append(dates, date) } return dates, rows.Err() } func hasOfficialRelease(db *sql.DB, date string) (bool, error) { var exists bool err := db.QueryRow(` WITH latest_prices AS ( SELECT rp.model_id, COALESCE(o.type, 'reseller') AS operator_type, ROW_NUMBER() OVER ( PARTITION BY rp.model_id ORDER BY rp.effective_date DESC NULLS LAST, rp.id DESC ) AS rn FROM region_pricing rp LEFT JOIN operator o ON rp.operator_id = o.id ) SELECT EXISTS ( SELECT 1 FROM models m LEFT JOIN latest_prices lp ON lp.model_id = m.id AND lp.rn = 1 WHERE m.deleted_at IS NULL AND m.release_date = $1::date AND COALESCE(m.source_url, '') <> '' AND COALESCE(lp.operator_type, 'reseller') IN ('official', 'cloud') ) `, date).Scan(&exists) return exists, err } func hasNewModel(db *sql.DB, date string) (bool, error) { var exists bool err := db.QueryRow(` SELECT EXISTS ( SELECT 1 FROM models WHERE deleted_at IS NULL AND DATE(created_at) = $1::date ) `, date).Scan(&exists) return exists, err } func hasPriceChange(db *sql.DB, date string) (bool, error) { var exists bool err := db.QueryRow(` SELECT EXISTS ( SELECT 1 FROM pricing_history WHERE DATE(changed_at) = $1::date AND ( COALESCE(old_input_price, 0) <> COALESCE(new_input_price, 0) OR COALESCE(old_output_price, 0) <> COALESCE(new_output_price, 0) ) ) `, date).Scan(&exists) return exists, err } func loadCoveragePromoDates() (map[string]bool, error) { path, err := resolveCoveragePromoDataPath() if err != nil { if os.IsNotExist(err) { return map[string]bool{}, nil } return nil, err } body, err := os.ReadFile(path) if err != nil { return nil, err } var definitions []CoveragePromoDefinition if err := json.Unmarshal(body, &definitions); err != nil { return nil, err } dates := make(map[string]bool, len(definitions)) for _, definition := range definitions { if strings.TrimSpace(definition.Date) != "" { dates[definition.Date] = true } } return dates, nil } func resolveCoveragePromoDataPath() (string, error) { candidates := []string{ filepath.Join("scripts", "testdata", "report_promo_campaigns.json"), filepath.Join("testdata", "report_promo_campaigns.json"), } for _, candidate := range candidates { if _, err := os.Stat(candidate); err == nil { return candidate, nil } } return "", os.ErrNotExist } func summarizeCoverage(days []CoverageDay) CoverageSummary { summary := CoverageSummary{TotalDays: len(days)} for _, day := range days { if day.HasTrueEvent() { summary.CoveredDays++ } } if summary.TotalDays > 0 { summary.CoveragePct = float64(summary.CoveredDays) * 100 / float64(summary.TotalDays) } return summary }